VFX для начинающих: создаем эффект нанесения урона по площади
Technical Artist Сергей Олейников рассказал о создании AoE-эффектов для мобильных игр на примере нового проекта Plarium Kharkiv.
Последние 2 года я работаю над RAID: Shadow Legends – пошаговой коллекционной RPG в сеттинге фентези. Ко всем эффектам в игре у нас были базовые требования: они должны хорошо выглядеть, передавать механики игры и быть понятными для игрока.
Кроме того, эффекты необходимо было переиспользовать, ведь в игре сотни персонажей, каждый из которых обладает несколькими способностями. Чтобы выпустить игру в срок, мы не привязывали эффекты к конкретным персонажным анимациям, а делали их достаточно универсальными, с возможностью легкой смены цвета.
В статье я расскажу про огненную волну, которая наносит урон сразу по группе противников.
Сбор референсов
Обязательный пункт, даже если у вас уже есть идея эффекта. Для этого отлично подходят азиатские сайты, например: https://www.cgjoy.com, http://huaban.com и http://bbs.cgwell.com.
Основные составляющие эффекта
Любой эффект можно разделить на 3 основных элемента: анимация, геометрия и материалы. Базовая анимация тут довольно простая и была сделана в самом движке Unity.
Геометрия состоит из хвоста, волны (верхняя и нижняя части) и трех систем частиц: дыма, тлеющего пепла и языков пламени на земле. Мы используем стандартную систему частиц Unity – Shuriken. Каждая система создает частицы в зависимости от пройденного расстояния (Emission: Rate over Distance).
Иногда при использовании шейдеров с прозрачностью можно получить артефакт при отрисовке – отдельные фейсы перерисовываются поверх остальных. Это произошло и с геометрией волны. Причин может быть несколько:
- геометрия сама себя перекрывает (например, торусы и их производные);
- геометрия получилась многослойной, и слои рисуются в неправильном порядке (частный случай первого варианта);
- геометрия была сшита из нескольких кусков в произвольном порядке (что и случилось с волной).
Первая проблема решается довольно просто: разрезаем геометрию, и Unity начинает правильно отрисовывать каждый из кусков. Со второй и третьей чуть интереснее. Скорее всего, у геометрии хаотичная нумерация вертексов, поэтому нужно в 3D редакторе сделать ренумерацию – и проблема решена. На гифке сверху хорошо видна зависимость отрисовки от нумерации вертексов. 1 – нумерация сверху вниз, 2 – рандомная, 3 – нумерация снизу вверх.
Основная визуальная нагрузка в этом эффекте легла на текстуры и шейдеры. На них и остановимся подробнее.
Хвост, волна и частицы пламени используют идентичные шейдеры, но есть нюансы.
- По хвосту можно двигать текстурную маску, и это создает эффект полета.
- Вертексная анимация придает волне объем и усиливает чувство движения.
- Скорость движения текстур в материалах не завязана на время, а контролируется вручную – это удобно для анимации. Например, когда волна замедляется, то замедляется не только скорость перемещения самой геометрии, но и движение текстур и вертексов.
- Для частиц пепла используется шейдер с альфа-эрозией. Таким образом, когда пепел тлеет, от него отделяются частички, тем самым уменьшая его размер.
- Частички дыма наименее интересны – это стандартный шейдер частиц, который идет из коробки.
Создавая шейдер огня, я сложил и перемножил текстуры, стараясь получить базу для материала, которая будет напоминать пламя.
half base = (R1 * G * (R0 + R1) + B) * B;
Я делал всё интуитивно, ориентируясь на подборку референсов, поэтому база прошла несколько итераций. В результате получилось 4 выборки из текстур. Это не самое дешевое решение в плане оптимизации, но результат того стоил.
Базу я покрасил с помощью gradient mapping. Это прием, при котором значения в черно-белой текстуре подменяются соответствующими значениями из цветной, что позволяет быстро и наглядно подбирать и менять цвета. На одну текстуру можно поместить все градиенты, используемые для эффектов в проекте, что помогает выдержать цветовую гамму. Это особенно важно, когда над одним проектом работает несколько художников по эффектам.
Для этого нужно считать градиент, используя в качестве одного из компонентов UV-координат нашу базу. half3 colBase = tex2D(_gradient, half2(base,0));
Если есть строгие требования по оптимизации (а это еще одна выборка текстуры, которая к тому же зависит от других выборок), то поможет серия интерполяций. Количество таких интерполяций напрямую зависит от количества цветов. half3 colBase = lerp(_Color1, _Color2, base);
Читабельность эффекта
Эффект должен хорошо выглядеть и читаться как на светлом, так и на темном фоне. А для этого нужно подобрать правильный блендинг. Alpha blending не дает ощущения свечения, которое нужно для материалов огня, молнии и т. д. Additive же на светлом фоне всё пересвечивает, и при этом теряются цвета. Есть третий вариант – Premultiplied Transparency с контролируемой альфой, который я и использовал.
Для этого в шейдере прописываем сам блендинг Blend One OneMinusSrcAlpha. Потом перемножаем финальный цвет на альфу (используем нашу базу). Получаем финальный цвет col.rgb = colBase * base; и альфа-канал col.a = base * _BlendingFactor; где BlendingFactor – слайдер со значениями от 0 до 1. Если _BlendingFactor = 0, то получим Additive blending, если 1 – обычный альфа-бленд. Я выставил 0,5 и получил нужный результат.
Теперь перейдем к системе частиц пепла. Чтобы добиться эффекта сгорания, я использовал шейдер, где анимация альфа-канала управляется через модуль custom vertex data.
Это усовершенствованный вариант альфа-эрозии, который отлично подходит для изображения всплесков жидкости или тлеющего пепла. В основе альфа-эрозии лежит простая идея: сделать исчезновение объекта более артистичным и неравномерным с помощью дополнительной черно-белой маски. К маске добавляем слайдер с диапазоном от 1 до -1, результат ограничиваем диапазоном от 1 до 0 и всё это умножаем на альфа-канал.
col.a = col.a * saturate(aMask + _slider);
Усовершенствование подхода заключается в том, что мы добавили возможность контролировать ширину и четкость границы маски. Если представить маску как карту высот, а нижнюю (_slider) и верхнюю (_slider + _step) плоскости как минимальное и максимальное значения, которые ремапятся в 0 и 1, то получим возможность двигаться по этой маске с определенным шагом (_step), который и будет отвечать за ширину нашей границы.
Советы
При компрессии текстур меньше всего страдают от артефактов сжатия красный и альфа-каналы, а больше всего – синий. Поэтому основополагающие текстуры, маски с четкими силуэтами или, например, декали лучше всего помещать в красный канал, а в синий – дополнительные паттерны или маски с размытыми и плавными границами.
Для сокращения количества текстурных выборок можно положить эту дополнительную маску в один из каналов текстуры.
Код и финальный результат:
col.a = (aMask - _slider) / (saturate(_slider + _step) - _slider);
Создание эффектов – сложная, комплексная и, самое главное, невероятно интересная работа. Начинающий VFX Artist должен понимать, как писать шейдеры, должен чувствовать анимацию, немного моделить, грамотно делать UV-развертку и хорошо знать такие компоненты игрового движка, как системы частиц и трейлы.
Не существует единственного правильного рецепта при создании VFX, решений всегда несколько, и это обеспечивает гибкость подхода. Если есть желание более подробно погрузиться в тему, рекомендую начать с этих ресурсов https://realtimevfx.com/ и https://simonschreibt.de/game-art-tricks/
И напоследок сделал видео с эффектами, которые я создавал для проекта RAID: Shadow Legends. Возможно, пригодится вам в качестве референсов.