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

Для воссоздания этого эффекта давайте начнём с чего-нибудь попроще. Например, с шейдера, по-разному раскрашивающего объект в зависимости от его положения. Для этого необходимо получить доступ к положению отрисовываемых пикселей в мире. Это можно выполнить, добавив поле
Затем можно использовать в функции поверхности координату Y положения в мире для изменения цвета объекта. Этого можно добиться изменением свойства
Результатом становится первое приближение к эффекту из Astroneer. Основная проблема заключается в том, что для цветной части всё ещё выполняется затенённое отображение.

В предыдущем туториале PBR and Lighting Models мы изучали способ создания собственных моделей освещения для поверхностных шейдеров. Неосвещённый шейдер всегда создаёт один и тот же цвет, вне зависимости от внешнего освещения и угла обзора. Можно реализовать его следующим образом:
Его единственная задача — возвращать единственный сплошной цвет. Как мы видим, он обращается к
Параметр

К сожалению, функция освещения не имеет доступа к положению объекта. Простейший способ предоставить эту информацию — использовать булеву переменную (
Последняя проблема, с которой нам предстоит столкнуться, довольно сложна. Как я объяснил в предыдущем разделе, мы можем использовать
В традиционном стандартном поверхностном шейдере директива
По стандартам наименования Unity легко заметить, что используемая функция должна называться
Мы хотим создать собственную функцию освещения под названием
Чтобы скомпилировать этот код, Unity 5 нужно определить ещё одну функцию:
Она используется для вычисления степени воздействия освещения на глобальное освещение, но для целей нашего туториала она необязательна.
Результат выйдет точно таким, какой нам нужен:

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

Теперь нас больше всего не устраивает то, что объект выглядит полым. Это не просто ощущение: в сущности, все 3D-модели являются полыми. Однако нам нужно создать иллюзию, что объект на самом деле сплошной. Этого с лёгкостью можно добиться, раскрашивая объект изнутри тем же неосвещённым шейдером. Объект по-прежнему полый, но воспринимается заполненным.
Чтобы достичь этого, мы просто раскрашиваем треугольники, направленные к камере обратной стороной. Если вы незнакомы с векторной алгеброй, то это может показаться достаточно сложным. На самом деле, этого можно довольно просто добиться с помощью скалярного произведения. Скалярное произведение двух векторов показывает, насколько они «сонаправлены». А это непосредственно связано с углом между ними. Когда скалярное произведение двух векторов отрицательно, то угол между ними больше 90 градусов. Мы можем проверить наше исходное условие, взяв скалярное произведение между направлением взгляда камеры (
Результат показан на изображениях ниже. Слева «изнаночная геометрия» отрендерена красным. Если использовать цвет верхней части объекта, то объект больше не выглядит полым.


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

Последняя часть эффекта — это анимация. Её можно получить, просто добавив к материалу параметр

Замечу в конце, что использованная в этом изображении модель несколько секунд выглядит полой, потому что нижняя часть ускорителей незамкнута. То есть объект на самом деле полый.
[Можно скачать пакет Unity (код, шейдер и 3D-модели), поддержав автора оригинала статьи десятью долларами на Patreon.]

Введение: первая попытка
Для воссоздания этого эффекта давайте начнём с чего-нибудь попроще. Например, с шейдера, по-разному раскрашивающего объект в зависимости от его положения. Для этого необходимо получить доступ к положению отрисовываемых пикселей в мире. Это можно выполнить, добавив поле
worldPos
к структуре Input
поверхностного шейдера Unity 5.struct Input {
float2 uv_MainTex;
float3 worldPos;
};
Затем можно использовать в функции поверхности координату Y положения в мире для изменения цвета объекта. Этого можно добиться изменением свойства
Albedo
в структуре SurfaceOutputStandard
.float _ConstructY;
fixed4 _ConstructColor;
void surf (Input IN, inout SurfaceOutputStandard o) {
if (IN.worldPos.y < _ConstructY)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
else
{
o.Albedo = _ConstructColor.rgb;
o.Alpha = _ConstructColor.a;
}
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
}
Результатом становится первое приближение к эффекту из Astroneer. Основная проблема заключается в том, что для цветной части всё ещё выполняется затенённое отображение.

Неосвещённый поверхностный шейдер
В предыдущем туториале PBR and Lighting Models мы изучали способ создания собственных моделей освещения для поверхностных шейдеров. Неосвещённый шейдер всегда создаёт один и тот же цвет, вне зависимости от внешнего освещения и угла обзора. Можно реализовать его следующим образом:
#pragma surface surf Unlit fullforwardshadows
inline half4 LightingUnlit (SurfaceOutput s, half3 lightDir, half atten)
{
return _ConstructColor;
}
Его единственная задача — возвращать единственный сплошной цвет. Как мы видим, он обращается к
SurfaceOutput
, который использовался в Unity 4. Если мы хотим создать собственную модель освещения, работающую с PBR и глобальным освещением, то нужно реализовать функцию, получающую в качестве входных данных SurfaceOutputStandard
. В Unity 5 для этого используется следующая функция:inline half4 LightingUnlit (SurfaceOutputStandard s, half3 lightDir, UnityGI gi)
{
return _ConstructColor;
}
Параметр
gi
здесь относится к глобальному освещению (global illumination), но в нашем неосвещённом шейдере он не выполняет никаких задач. Такой подход работает, но у него есть большая проблема. Unity не позволяет поверхностному шейдеру выборочно изменять функцию освещения. Мы не можем применить стандартное освещение по Ламберту к нижней части объекта и одновременно сделать верхнюю часть неосвещённой. Можно назначить единственную функцию освещения для всего объекта. Мы должны сами менять способ рендеринга объекта в зависимости от его положения.
Передаём параметры функции освещения
К сожалению, функция освещения не имеет доступа к положению объекта. Простейший способ предоставить эту информацию — использовать булеву переменную (
building
), которую мы зададим в функции поверхности. Эту переменную может проверять наша новая функция освещения.int building;
void surf (Input IN, inout SurfaceOutputStandard o) {
if (IN.worldPos.y < _ConstructY)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
building = 0;
}
else
{
o.Albedo = _ConstructColor.rgb;
o.Alpha = _ConstructColor.a;
building = 1;
}
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
}
Расширяем стандартную функцию освещения
Последняя проблема, с которой нам предстоит столкнуться, довольно сложна. Как я объяснил в предыдущем разделе, мы можем использовать
building
для изменения способа вычисления освещения. Часть объекта, которая в текущий момент строится, будет неосвещённой, а на оставшейся части будет правильно рассчитанное освещение. Если мы хотим, чтобы наш материал использовал PBR, мы не можем переписывать весь код для фотореалистичного освещения. Единственное разумное решение — вызывать стандартную функцию освещения, которая уже реализована в Unity.В традиционном стандартном поверхностном шейдере директива
#pragma
, определяющая использование функции освещения PBR, имеет следующий вид:#pragma surface surf Standard fullforwardshadows
По стандартам наименования Unity легко заметить, что используемая функция должна называться
LightingStandard
. Эта функция находится в файле UnityPBSLighting.cginc
, который можно при необходимости подключить.Мы хотим создать собственную функцию освещения под названием
LightingCustom
. В обычных условиях она просто вызывает стандартную функцию PBR из Unity под названием LightingStandard
. Однако при необходимости она использует определённую ранее LightingUnlit
.inline half4 LightingCustom(SurfaceOutputStandard s, half3 lightDir, UnityGI gi)
{
if (!building)
return LightingStandard(s, lightDir, gi); // Unity5 PBR
return _ConstructColor; // Unlit
}
Чтобы скомпилировать этот код, Unity 5 нужно определить ещё одну функцию:
inline void LightingCustom_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
{
LightingStandard_GI(s, data, gi);
}
Она используется для вычисления степени воздействия освещения на глобальное освещение, но для целей нашего туториала она необязательна.
Результат выйдет точно таким, какой нам нужен:

В этой первой части мы научились использовать две разные модели освещения в одном шейдере. Это позволило нам отрендерить одну половину модели с использованием PBR, а другую оставить неосвещённой. Во второй части мы завершим этот туториал и покажем, как анимировать и улучшить эффект.
Отрезаем геометрию
Проще всего добавить к нашему шейдеру эффект прекращения отрисовки верхней части геометрии. Для отмены отрисовки произвольного пикселя в шейдере можно использовать ключевое слово
discard
. С его помощью можно отрисовывать только границу вокруг верхней части модели:void surf (Input IN, inout SurfaceOutputStandard o)
{
if (IN.worldPos.y > _ConstructY + _ConstructGap)
discard;
...
}
Важно помнить, что это может оставлять «дыры» в нашей геометрии. Нужно отключить отсечение граней, чтобы полностью отрисовывалась обратная сторона объекта.
Cull Off

Теперь нас больше всего не устраивает то, что объект выглядит полым. Это не просто ощущение: в сущности, все 3D-модели являются полыми. Однако нам нужно создать иллюзию, что объект на самом деле сплошной. Этого с лёгкостью можно добиться, раскрашивая объект изнутри тем же неосвещённым шейдером. Объект по-прежнему полый, но воспринимается заполненным.
Чтобы достичь этого, мы просто раскрашиваем треугольники, направленные к камере обратной стороной. Если вы незнакомы с векторной алгеброй, то это может показаться достаточно сложным. На самом деле, этого можно довольно просто добиться с помощью скалярного произведения. Скалярное произведение двух векторов показывает, насколько они «сонаправлены». А это непосредственно связано с углом между ними. Когда скалярное произведение двух векторов отрицательно, то угол между ними больше 90 градусов. Мы можем проверить наше исходное условие, взяв скалярное произведение между направлением взгляда камеры (
viewDir
в поверхностном шейдере) и нормалью треугольника. Если оно отрицательное, то треугольник повёрнут от камеры. То есть мы видим его «изнанку» и можем отрендерить её сплошным цветом.struct Input {
float2 uv_MainTex;
float3 worldPos;
float3 viewDir;
};
void surf (Input IN, inout SurfaceOutputStandard o)
{
viewDir = IN.viewDir;
...
}
inline half4 LightingCustom(SurfaceOutputStandard s, half3 lightDir, UnityGI gi)
{
if (building)
return _ConstructColor;
if (dot(s.Normal, viewDir) < 0)
return _ConstructColor;
return LightingStandard(s, lightDir, gi);
}
Результат показан на изображениях ниже. Слева «изнаночная геометрия» отрендерена красным. Если использовать цвет верхней части объекта, то объект больше не выглядит полым.

Эффект «волнистости»

Если вы играли в Planetary Annihilation, то знаете, что в шейдере 3D-принтера используется эффект небольшой волнистости. Мы тоже можем его реализовать, добавив немного шума к положению отрисовываемых пикселей в мире. Этого можно добиться или текстурой шума, или с помощью непрерывной периодической функции. В коде ниже я использую синусоиду с произвольными параметрами.
void surf (Input IN, inout SurfaceOutputStandard o)
{
float s = +sin((IN.worldPos.x * IN.worldPos.z) * 60 + _Time[3] + o.Normal) / 120;
if (IN.worldPos.y > _ConstructY + s + _ConstructGap)
discard;
...
}
Эти параметры можно подправить вручную для получения красивого эффекта волнистости.

Анимация
Последняя часть эффекта — это анимация. Её можно получить, просто добавив к материалу параметр
_ConstructY
. Об остальном позаботится шейдер. Можно управлять скоростью эффекта или через код, или с помощью кривой анимации. При первом варианте вы можете полностью контролировать его скорость.public class BuildingTimer : MonoBehaviour
{
public Material material;
public float minY = 0;
public float maxY = 2;
public float duration = 5;
// Update is called once per frame
void Update () {
float y = Mathf.Lerp(minY, maxY, Time.time / duration);
material.SetFloat("_ConstructY", y);
}
}

Замечу в конце, что использованная в этом изображении модель несколько секунд выглядит полой, потому что нижняя часть ускорителей незамкнута. То есть объект на самом деле полый.
[Можно скачать пакет Unity (код, шейдер и 3D-модели), поддержав автора оригинала статьи десятью долларами на Patreon.]
Поделиться с друзьями
alt3d
Мне кажется можно было обойтись без кастомной модели освещения просто отрисовывая верхнуюю часть «светящейся». Визуально результат был-бы очень похожим.
И везде пишут что нужно избегать if-ов в шейдерах.
nightrain912
Можно, но такой шейдер будет сложнее (из-за полноценной модели освещения для пикселей, которым она не нужна).
Да, нужно избегать, но это ведь не жесткое правило: чем меньше условных переходов, тем быстрее. Просто они довольно тяжелые. Опять же, если их используют в вертексном шейдере, это приемлемо, а вот в фрагментном уже не стоит.