Туториал по созданию эффекта сел-шейдинга в Unity

Поэтапный рассказ с примерами кода.

Эффект сел-шейдинга используется для стилизации графики под мультфильм — благодаря этому 3D-объекты выглядят как 2D-спрайты. Эта техника рендеринга стала популярной после нескольких крайне успешных игр, к примеру, Jet Set Radio и The Legend of Zelda: The Wind Waker.

Независимый разработчик Эрик Ройстен Росс в 2018 году в собственном блоге опубликовал туториал, в котором подробно рассказал, как реализовать эффект сел-шейдинга в Unity. Мы выбрали из текста главное.

Туториал по созданию эффекта сел-шейдинга в Unity

Основной референс в этом туториале — The Legend of Zelda: Breath of the Wild.

Для создания нужного эффекта используется один направленный источник света, отражающие поверхности, чёткое разделение на свет и тень а также контровой свет
Для создания нужного эффекта используется один направленный источник света, отражающие поверхности, чёткое разделение на свет и тень а также контровой свет

Сначала скачайте стартовый проект и откройте его в Unity. Этот файл содержит простой шейдер — по умолчанию текстура окрашена в синий цвет.

Туториал по созданию эффекта сел-шейдинга в Unity

Направленный источник света

Если дело касается шейдеров, которые взаимодействуют с освещением, то обычно используются Surface Shaders — они позволяют автоматизировать взаимодействие объекта со светом и глобальным освещением. Но в нашем случае шейдер будет взаимодействовать только с направленным источником светом, поэтому в Surface Shaders нет необходимости.

Первым делом нужно сделать так, чтобы шейдер получал данные об освещении. Добавьте следующий код в верхней части Pass — сразу после фигурной скобки.

Tags { "LightMode" = "ForwardBase" "PassFlags" = "OnlyDirectional" }

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

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

Векторы затенения Блинна-Фонга. L — вектор к источнику света, N — нормаль поверхности
Векторы затенения Блинна-Фонга. L — вектор к источнику света, N — нормаль поверхности

Шейдер должен учитывать данные нормалей, поэтому потребуется следующий код.

// Inside the appdata struct. float3 normal : NORMAL; … // Inside the v2f struct. float3 worldNormal : NORMAL;

Нормали в appdata заполняются автоматически, в то время как значения в v2f должны быть введены вручную в vertex shader. Также нужно преобразовать нормаль из object space в world space, поскольку направление света связано именно с world space, Добавьте следующую строку в vertex shader.

o.worldNormal = UnityObjectToWorldNormal(v.normal);

Теперь нормаль можно сопоставить с направлением света при помощи скалярного произведения.

Скалярное произведение сравнивает два вектора и выдаёт простое число, основываясь на том, под каким углом они находятся по отношению друг к другу. Если векторы параллельны, то число равно 1, если перпендикулярны, то 0. Когда угол между векторами больше 90, скалярное произведение будет отрицательным. Добавьте в шейдер следующий код.

// At the top of the fragment shader. float3 normal = normalize(i.worldNormal); float NdotL = dot(_WorldSpaceLightPos0, normal); … // Modify the existing return line. return _Color * sample * NdotL;
Туториал по созданию эффекта сел-шейдинга в Unity

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

// Below the NdotL declaration. float lightIntensity = NdotL > 0 ? 1 : 0; … return _Color * sample * lightIntensity;
Это уже больше похоже на сел-шейдинг, но тёмная часть получилась слишком тёмной. Также граница между разными зонами выглядит слишком резкой
Это уже больше похоже на сел-шейдинг, но тёмная часть получилась слишком тёмной. Также граница между разными зонами выглядит слишком резкой

Рассеянный свет

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

// Add as a new property. [HDR] _AmbientColor("Ambient Color", Color) = (0.4,0.4,0.4,1) … // Matching variable, add above the fragment shader. float4 _AmbientColor; … return _Color * sample * (_AmbientColor + lightIntensity);
Туториал по созданию эффекта сел-шейдинга в Unity

Чтобы можно было менять интенсивность или цвет направленного света, нужно добавить ещё одну часть кода.

// Add below the existing #include "UnityCG.cginc" #include "Lighting.cginc" … // Add below the lightIntensity declaration. float4 light = lightIntensity * _LightColor0; … return _Color * sample * (_AmbientColor + light);

Пора смягчить границу между светом и тенью. Для этого используется функция smoothstep: у неё есть три значения — нижняя граница, верхняя граница и значение между ними. Важно понимать, что функция smoothstep не линейная: в значениях от 0 до 0,5 она «усиливается», а в значениях от 0,5 до 1 — «ослабляется».

Слева — пример не линейной функции smoothstep. Справа — линейная функция
Слева — пример не линейной функции smoothstep. Справа — линейная функция
float lightIntensity = smoothstep(0, 0.01, NdotL);

Отражения

Следующий шаг — добавление отражений: они зависят от угла, под которым на него смотрят. Поэтому нужно рассчитать world view direction в vertex shader.

// Add to the v2f struct. float3 viewDir : TEXCOORD1; … // Add to the vertex shader. o.viewDir = WorldSpaceViewDir(v.vertex);

Теперь мы переходим к реализации модели отражений Блинна-Фонга. Этот расчёт учитывает два свойства поверхности: цвет отражения и то, насколько хорошо поверхность отражает свет.

// Add as new properties. [HDR] _SpecularColor("Specular Color", Color) = (0.9,0.9,0.9,1) _Glossiness("Glossiness", Float) = 32 … // Matching variables. float _Glossiness; float4 _SpecularColor;

Сила отражения в модели Блинна-Фонга определяется как скалярное произведение между нормалью поверхности и half-вектором — вектором между углом обзора и источником света. Чтобы получить half-вектор, нужно суммировать эти два вектора, а затем нормализовать результат.

// Add to the fragment shader, above the line sampling _MainTex. float3 viewDir = normalize(i.viewDir); float3 halfVector = normalize(_WorldSpaceLightPos0 + viewDir); float NdotH = dot(normal, halfVector); float specularIntensity = pow(NdotH * lightIntensity, _Glossiness * _Glossiness); … return _Color * sample * (_AmbientColor + light + specularIntensity);

Функция pow контролирует размер отражения. Также нужно умножить NdotH на lightIntensity, чтобы гарантировать, что отражения появляются только в том случае, если поверхность освещена.

Туториал по созданию эффекта сел-шейдинга в Unity

Затем нужно ещё раз использовать smoothstep для усиления отражения, а также умножить конечный результат на _SpecularColor.

// Add below the specularIntensity declaration. float specularIntensitySmooth = smoothstep(0.005, 0.01, specularIntensity); float4 specular = specularIntensitySmooth * _SpecularColor; … return _Color * sample * (_AmbientColor + light + specular);
Туториал по созданию эффекта сел-шейдинга в Unity

Контровое освещение

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

Само контровое освещение можно определить как поверхность, которая обращена в сторону от камеры. Чтобы вычислить это освещение, нужно взять скалярное произведение нормали и направления камеры, а затем инвертировать его.

// In the fragment shader, below the line declaring specular. float4 rimDot = 1 - dot(viewDir, normal); … return _Color * sample * (_AmbientColor + light + specular + rimDot);
Туториал по созданию эффекта сел-шейдинга в Unity

Чтобы усилить эффект мультяшности, нужно установить пороговое значение с помощью smoothstep.

// Add as new properties. [HDR] _RimColor("Rim Color", Color) = (1,1,1,1) _RimAmount("Rim Amount", Range(0, 1)) = 0.716 … // Matching variables. float4 _RimColor; float _RimAmount; … // Add below the line declaring rimDot. float rimIntensity = smoothstep(_RimAmount - 0.01, _RimAmount + 0.01, rimDot); float4 rim = rimIntensity * _RimColor; … return _Color * sample * (_AmbientColor + light + specular + rim);
Туториал по созданию эффекта сел-шейдинга в Unity

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

// Add above the existing rimIntensity declaration, replacing it. float rimIntensity = rimDot * NdotL; rimIntensity = smoothstep(_RimAmount - 0.01, _RimAmount + 0.01, rimIntensity);

С помощью функции pow можно масштабировать контровой свет.

// Add as a new property. _RimThreshold("Rim Threshold", Range(0, 1)) = 0.1 … // Matching variable. float _RimThreshold; … float rimIntensity = rimDot * pow(NdotL, _RimThreshold);
Туториал по созданию эффекта сел-шейдинга в Unity

Тени

Финальный шаг — добавление возможности отбрасывать тени. Поставьте следующий фрагмент перед Pass.

// Insert just after the closing curly brace of the existing Pass. UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"

Намного сложнее реализовать возможность, при которой тени будут падать на сам объект. Для этого нужно сделать так, чтобы шейдер «знал», когда объект попадает в тень — нужно перенести координаты текстуры из vertex shader в fragment shader.

// As a new include, below the existing ones. #include "AutoLight.cginc" … // Add to the v2f struct. SHADOW_COORDS(2) … // Add to the vertex shader. TRANSFER_SHADOW(o)

Для реализации этого, нужно добавить Autolight.cginc — файл, содержащий несколько макросов, которые используются для определения теней. SHADOW_COORDS (2) генерирует значение для четырёх измерений с меняющейся точностью и присваивает его семантике TEXCOORD по заданному индексу (в нашем случае 2).

TRANSFER_SHADOW преобразует пространство вершины в пространство теневой карты, а затем сохраняет его в SHADOW_COORD.

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

#pragma multi_compile_fwdbase

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

// In the fragment shader, above the existing lightIntensity declaration. float shadow = SHADOW_ATTENUATION(i); float lightIntensity = smoothstep(0, 0.01, NdotL * shadow);
Туториал по созданию эффекта сел-шейдинга в Unity

В результате получается эффект сел-шейдинга, имитирующий 2D-стиль рисования.

204204
41 комментарий

Комментарий недоступен

71
Ответить

жесть ты крут

15
Ответить

Уух, тоже замотивировался. Вот все, начинаю работу и сделаю тоже крутой проект. 
...  
А нет, все я потерял интерес, проект заброшен. 

12
Ответить

вот ты молодец! иди и делай! мы в тебя верим! 

4
Ответить

Комментарий недоступен

1
Ответить

У меня этот тутор настолько долго в закладках лежал, что его уже успели перевести...

7
Ответить

Unity мертвый движок. Оставьте вы его уже.

Ответить