В этом туториале мы воссоздадим эффект 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.]
