Создание шейдерного эффекта 3D-принтера

в 10:15, , рубрики: unity туториал, unity3d, программирование шейдеров, разработка игр, шейдеры

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

Создание шейдерного эффекта 3D-принтера - 1

Введение: первая попытка

Для воссоздания этого эффекта давайте начнём с чего-нибудь попроще. Например, с шейдера, по-разному раскрашивающего объект в зависимости от его положения. Для этого необходимо получить доступ к положению отрисовываемых пикселей в мире. Это можно выполнить, добавив поле 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. Основная проблема заключается в том, что для цветной части всё ещё выполняется затенённое отображение.

image

Неосвещённый поверхностный шейдер

В предыдущем туториале 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 не позволяет поверхностному шейдеру выборочно изменять функцию освещения. Мы не можем применить стандартное освещение по Ламберту к нижней части объекта и одновременно сделать верхнюю часть неосвещённой. Можно назначить единственную функцию освещения для всего объекта. Мы должны сами менять способ рендеринга объекта в зависимости от его положения.

image

Передаём параметры функции освещения

К сожалению, функция освещения не имеет доступа к положению объекта. Простейший способ предоставить эту информацию — использовать булеву переменную (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);		
}

Она используется для вычисления степени воздействия освещения на глобальное освещение, но для целей нашего туториала она необязательна.

Результат выйдет точно таким, какой нам нужен:

image

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

Отрезаем геометрию

Проще всего добавить к нашему шейдеру эффект прекращения отрисовки верхней части геометрии. Для отмены отрисовки произвольного пикселя в шейдере можно использовать ключевое слово discard. С его помощью можно отрисовывать только границу вокруг верхней части модели:

void surf (Input IN, inout SurfaceOutputStandard o)
{
	if (IN.worldPos.y > _ConstructY + _ConstructGap)
		discard;
 
	...
}

Важно помнить, что это может оставлять «дыры» в нашей геометрии. Нужно отключить отсечение граней, чтобы полностью отрисовывалась обратная сторона объекта.

Cull Off

image

Теперь нас больше всего не устраивает то, что объект выглядит полым. Это не просто ощущение: в сущности, все 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);
}

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

image

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

image

Если вы играли в 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;
	
	...
}

Эти параметры можно подправить вручную для получения красивого эффекта волнистости.

image

Анимация

Последняя часть эффекта — это анимация. Её можно получить, просто добавив к материалу параметр _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);
    }
}

image

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

[Можно скачать пакет Unity (код, шейдер и 3D-модели), поддержав автора оригинала статьи десятью долларами на Patreon.]

Автор: PatientZero

Источник

Поделиться

* - обязательные к заполнению поля