Хабрахабр

[Перевод] Шейдерный эффект дудла

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

Последние несколько лет этот стиль становится всё более популярным и активно используется в таких играх, как GoNNER и Baba is You.

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

На создание этого туториала меня вдохновил успех Doodle Studio 95!.

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

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

(см. Если вы ищете профессиональный и эффективный способ анимирования 2D-спрайтов с большой степенью художественного контроля, то я крайне рекомендую вам Doodle Studio 95! Здесь можно посмотреть на некоторые игры, в которых используется этот инструмент. GIF ниже).

Для воссоздания эффекта дудла нам сначала нужно понять, как он работает и какие техники в нём применяются.

Это возможно благодаря использованию шейдеров, сообщающих Unity, как рендерить 3D-модели (в том числе и плоские!) на экране. Шейдерный эффект. Во-первых, мы хотим, чтобы этот эффект был как можно проще и не требовал дополнительных скриптов. Если вы незнакомы с миром кодирования шейдеров, то вам стоит изучить мою статью A Gentle Introduction to Shaders.

Если вы пользуетесь предоставляемыми Unity 2D-инструментами, то, скорее всего, работаете со спрайтами. Спрайтовый шейдер. В комплекте Unity есть множество типов шейдеров. Или же можно начать с более традиционного Unlit shader. Если это так, то вам нужен Sprite Shader — особый тип шейдеров, совместимый с SpriteRenderer Unity.

Мы хотим заставить спрайт каким-то образом «колебаться», чтобы симулировать этот эффект. Смещение вершин. При отрисовке спрайтов вручную ни один кадр не будет полностью совпадать с другими. Это техника, позволяющая менять позицию вершин 3D-объекта. В шейдерах есть очень эффективный способ для этого, применяющий смещение вершин. Если мы случайным образом сдвинем их, то получим желаемый эффект.

Если мы хотим симулировать, допустим, пять кадров в секунду, то нужно менять позицию вершин спрайтов по пять раз в секунду. Время перемещения. Рисуемые от руки анимации обычно имеют низкую частоту кадров. Чтобы спрайт не менялся по 60 раз в секунду, нужно поработать над компонентом таймингов анимации. Однако Unity скорее всего будет выполнять игру с гораздо более высокой частотой обновления; возможно, с 30 или даже 60 кадрами в секунду.

Если вы захотите создать новый шейдер в Unity, то выбор будет довольно ограниченным. Самым близким шейдером, с которого мы можем начать, будет Unlit Shader, хоть он и не обязательно лучше всего подходит для наших целей.

К сожалению, мы не можем получить к нему доступ непосредственно из самого Unity. Если вы хотите, чтобы шейдер дудла был полностью совместим со SpriteRenderer Unity, то нам нужно дополнить существующий Sprite Shader.

Это zip-файл, содержащий исходный код всех шейдеров, поставляемых с вашей сборкой Unity. Добраться до него можно, зайдя на страницу Unity download archive и скачав пакет Build in shaders для той версии Unity, с которой вы работаете.

После скачивания распакуйте его и найдите в папке builtin_shaders-2018.1.6f1\DefaultResourcesExtra файл Sprites-Diffuse.shader. Именно этот файл мы будем использовать в туториале.

Sprites-Diffuse не является стандартным спрайтовым шейдером!

При создании нового спрайта его стандартный материал использует шейдер под названием Sprites-Default.shader, а не Sprites-Diffuse.shader.

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

В конце этого туториала есть ссылка на скачивание шейдеров дудла с учётом освещения и без него.

Внутри Sprites-Diffuse.shader есть функция под названием vert, являющаяся вершинной функцией, о которой мы говорили выше. Её название не важно, главное, чтобы оно совпадало с указанным в разделе vertex: директивы #pragma:

#pragma surface surf Lambert vertex:vert nofog nolightmap nodynlightmap keepalpha noinstancing

Если говорить вкратце, то вершинная функция вызывается для каждой вершины 3D-модели и решает, как наложить её на двухмерное пространство экрана. В данном туториале нас интересует только, как сместить объект.

При изменении значения вершина сдвигается. Параметр appdata_full v содержит поле vertex, в котором содержится 3D-позиция каждой вершины в пространстве объекта. То есть, например, показанный ниже код перенесёт объект с его шейдером на одну единицу вдоль оси X.

void vert (inout appdata_full v, out Input o)
{ v.vertex = UnityFlipSprite(v.vertex, _Flip); v.vertex.x += 1; #if defined(PIXELSNAP_ON) v.vertex = UnityPixelSnap (v.vertex); #endif UNITY_INITIALIZE_OUTPUT(Input, o); o.color = v.color * _Color * _RendererColor;
}

По умолчанию создаваемые в Unity 2D-игры оперируют только с осями X и Y, поэтому нам нужно изменить v.vertex.xy, чтобы переместить спрайт на двухмерной плоскости.

Что такое пространство объекта?

Поле vertex структуры appdata_full содержит позицию текущей вершины, обрабатываемой шейдером в пространстве объекта. Это позиция вершины при допущении, что объект расположен в центре мира (0,0,0), без изменения масштаба и без поворота.

Вершины, выраженные в мировом пространстве, отражают их реальное положение в сцене Unity.

Почему объект не движется со скоростью один метр за кадр?

Если мы будем прибавлять +1 к компоненту x величины transform.position в методе Update скрипта на языке C#, то увидим, как объект летит вправо со скоростью 1 метр на кадр, то есть примерно 216 километров в час.

В вершинной функции этого не происходит. Так происходит, потому что вносимые C# изменения меняют саму позицию. Именно поэтому добавление +1 к v.vertex.x смещает объект на метр только один раз. Шейдер меняет только визуальное представление модели, но не обновляет и не изменяет хранящиеся вершины модели.

Не забудьте импортировать спрайт как Tight!

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

справа на рисунке). Для более сильного и реалистичного искажения нужно импортировать спрайты, выбрав для параметра Mesh Type значение Tight, которое превращает их в выпуклую фигуру (см.

Это увеличивает количество вершин. Такое не всегда желательно, но именно это нам сейчас и нужно.

Случайное смещение

Эффект дудла случайным образом смещает позицию каждой вершины. Сэмплирование случайных чисел в шейдере всегда было сложной задачей. В основном это вызвано распределённой архитектурой GPU, усложняющей и снижающей эффективность воссоздания алгоритма, используемого в большинстве библиотек (в том числе и Mathf.Random).

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

И поскольку они должны быть распределёнными, каждое случайное число должно генерироваться со своим собственным seed. Поэтому в большинстве шейдеров применяются довольно запутанные и хаотичные функции, которые, несмотря на детерминированность, выглядят для нас не имеющими закономерностей. Мы можем использовать это, чтобы привязать к каждой вершине случайное число. Это отлично нам подходит, потому что позиция каждой вершины должна быть уникальной. Реализацию этой функции случайности мы обсудим позже; пока назовём её random3.

В показанном ниже примере случайные числа масштабируются с помощью свойства _NoiseScale, позволяющего контролировать силу смещения. Мы можем использовать random3 для генерации случайного смещения каждой вершины.

void vert (inout appdata_full v, out Input o)
{ ... float2 noise = random3(v.vertex.xyz).xy * _NoiseScale; v.vertex.xy += noise; ...
}

Теперь нам нужно написать сам код random3.

image

Случайность в шейдере

Одна из самых распространённых и знаковых псевдослучайных функций, используемых в шейдерах, взята из статьи 1998 года В. Рея под названием "On generating random numbers, with help of y= [(a+x)sin(bx)] mod 1".

float rand(float2 co)
{ return fract(sin(dot(co.xy ,float2(12.9898,78.233))) * 43758.5453);
}

Эта функция детерминирована (то есть она не по-настоящему случайна), но ведёт себя настолько хаотично, что выглядит совершенно случайной. Такие функции называются псевдослучайными. Для своего туториала я выбрал более сложную функцию, придуманную Никитой Миропольским

Если вам интересно узнать о ней больше, то в The Book of Shaders есть хорошая глава, посвящённая ей. Генерирование псевдослучайного числа в шейдере — это очень сложная тема. Кроме того, Патрисио Гонзалес Виво собрал большой репозиторий псевдослучайных функций под названием GLSL noise, которые можно использовать в шейдерах.

Благодаря написанному нами коду каждая точка смещается в каждом кадре на одну и ту же величину. Так мы получаем искажённый спрайт, а не эффект дудла. Чтобы исправить это, нужно найти способ менять эффект с течением времени. Один из самых простых способов сделать это — использовать для генерации случайного числа и позицию вершины, и текущее время.

В нашем случае я просто добавил текущее время в секундах _Time.y к позиции вершины.

float time = float3(_Time.y, 0, 0);
float2 noise = random3(v.vertex.xyz + time).xy * _NoiseScale;
v.vertex.xy += noise;

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

Переключение времени

Основная проблема с добавлением _Time.y заключается в том, что оно заставляет спрайт анимироваться в каждом кадре. Это для нас нежелательно, потому что большинство нарисованных от руки анимаций имеет низкую частоту кадров. Компонент времени должен быть не непрерывным, а дискретным. Это значит, что если мы хотим отображать пять кадров в секунду, то он должен меняться только пять раз в секунду. То есть время должно быть привязано к одной пятой секунды. Единственными допустимыми значениями должны быть $\frac{5} = 0$, $\frac{1}{5} = 0.2$, $\frac{2}{5} = 0.4$, $\frac{3}{5} = 0.6$, $\frac{4}{5} = 0.8$, $\frac{5}{5} = 1$ с, и так далее…

В этой статье я предложил решение задачи привязки позиции объекта на пространственной сетке. Я уже рассматривал привязку в своём блоге, в статье How To Snap To Grid. Если нам нужно привязать время к временной сетке, то математика, а значит и код, будут теми же.

Показанная ниже функция берёт число x и привязывает его к целочисленным значениям, кратным snap.

inline float snap (float x, float snap)
{ return snap * round(x / snap);
}

То есть наш код становится таким:

float time = snap(_Time.y, _NoiseSnap);
float2 noise = random3(v.vertex.xyz + float3(time, 0.0, 0.0) ).xy * _NoiseScale;
v.vertex.xy += noise;

Пакет Unity для этого эффекта можно платно скачать на Patreon.

Дополнительные ресурсы

За последние несколько месяцев появилось большое количество игр в стилистике дудлов. Мне кажется, что причиной этого стал успех Doodle Studio 95! — инструмента для Unity, разработанного Фернандо Рамальо. Если этот стиль подходит к вашей игре, то рекомендую купить этот потрясающий инструмент.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»