Часть 1: шейдер растворения
Шейдер растворения возвращает красивый эффект, к тому же его легко создать и понять; сегодня мы сделаем его в Unity Shader Graph, а также напишем на HLSL.
Вот пример того, что мы будем создавать:
Как это работает
Чтобы создать шейдер растворения (dissolve shader), нам придётся работать со значением AlphaClipThreshold в шейдере «Shader Graph» или воспользоваться функцией HLSL под названием clip.
По сути, мы прикажем шейдеру не рендерить пиксель на основании текстуры и переданного значения. Нам нужно знать следующее: белые части растворяются быстрее.
Использовать мы будем такую текстуру:
Можете создать и собственную — прямые, треугольники, да всё, что угодно! Просто помните, что белые части растворяются быстрее.
Эту текстуру я создал в Photoshop, воспользовавшись фильтром «Clouds».
Даже если вам интересен только Shader Graph и вы ничего не знаете о HLSL, то я всё равно рекомендую прочитать эту часть, потому что полезно понимать, как Unity Shader Graph работает внутри.
HLSL
В HLSL мы используем функцию clip(x). Функция clip(x) отбрасывает все пиксели со значением меньше нуля. Поэтому если мы вызовем clip(-1), то будем уверены, что шейдер никогда не будет рендерить этот пиксель. Подробнее о clip можно прочитать в Microsoft Docs.
Свойства
Шейдеру нужны два свойства, Dissolve Texture и Amount (которая будет обозначать общий процесс выполнения). Как и в случае с другими свойствами и переменными, можно назвать их как угодно.
Properties {
//Your other properties
//[...]
//Dissolve shader properties
_DissolveTexture("Dissolve Texture", 2D) = "white" {}
_Amount("Amount", Range(0,1)) = 0
}
Не забудьте добавить после CGPROGRAM SubShader следующее (иными словами, объявить переменные):
sampler2D _DissolveTexture;
half _Amount;
Кроме того, не забудьте. что их имена должны соответствовать именам в разделе Properties.
Функция
Мы начнём функцию Surface или Fragment с сэмплирования текстуры растворения и получения значения красного. P.S. Наша текстура сохранена в градациях серого, то есть её значения R, G и B равны, и можно выбрать любое из них. Например, белый равен (1,1,1), чёрный равен (0,0,0).
В своём примере я используют поверхностный шейдер:
void surf (Input IN, inout SurfaceOutputStandard o) {
half dissolve_value = tex2D(_DissolveTexture, IN.uv_MainTex).r; //Get how much we have to dissolve based on our dissolve texture
clip(dissolve_value - _Amount); //Dissolve!
//Your shader body, you can set the Albedo etc.
//[...]
}
И всё! Мы можем применить этот процесс к любому имеющемуся шейдеру и превратить его в шейдер растворения!
Вот стандартный Surface Shader движка Unity, превращённый в двусторонний шейдер растворения:
Shader "Custom/DissolveSurface" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
//Dissolve properties
_DissolveTexture("Dissolve Texutre", 2D) = "white" {}
_Amount("Amount", Range(0,1)) = 0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
Cull Off //Fast way to turn your material double-sided
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
//Dissolve properties
sampler2D _DissolveTexture;
half _Amount;
void surf (Input IN, inout SurfaceOutputStandard o) {
//Dissolve function
half dissolve_value = tex2D(_DissolveTexture, IN.uv_MainTex).r;
clip(dissolve_value - _Amount);
//Basic shader function
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
Shader Graph
Если нам нужно будет создать этот эффект с помощью Unity Shader Graph, то мы должны использовать значение AlphaClipThreshold (которое работает иначе, чем clip(x) из HLSL). В этом примере я создал PBR-шейдер.
Функция AlphaClipThreshold приказывает шейдеру отбрасывать все пиксели, значение которых меньше её значения Alpha. Например, если оно равно 0.3f, а наше значение альфы равно 0.2f, то шейдер не будет рендерить этот пиксель. О функции AlphaClipThreshold можно прочитать в документации Unity: PBR Master Node и Unlit Master Node.
Вот наш готовый шейдер:
Мы сэмплируем текстуру растворения и получаем значение красного, а затем прибавляем его к значению Amount (которое является свойством, которое я добавил для обозначения общего процесса выполнения, значение 1 означает полное растворение) и соединяем его к AlphaClipThreshold. Готово!
Если вы хотите применить его к любому имеющемуся шейдеру, то просто скопируйте соединения нодов в AlphaClipThreshold (не пропустите необходимые свойства!). Также можно сделать его двусторонним и получить ещё более красивый результат!
Шейдер растворения с контурами
А если попробовать добавить к нему контуры? Давайте это сделаем!
Мы не можем работать с уже растворившимися пикселями, потому что после их отбрасывания они пропадают навсегда. Вместо этого мы можем работать с «почти растворившимися» значениями!
В HLSL это сделать очень просто, достаточно добавить несколько строк кода после вычислений clip:
void surf (Input IN, inout SurfaceOutputStandard o) {
//[...]
//After our clip calculations
if (dissolve_value - _Amount < .05f) //outline width = .05f
o.Emission = fixed3(1, 1, 1); //emits white color
//Your shader body, you can set the Albedo etc.
//[...]
}
Готово!
При работе с Shader Graph логика немного отличается. Вот готовый шейдер:
Мы можем создать очень крутые эффекты с помощью простого шейдера растворения; можно экспериментировать с различными текстурами и значениями, а также придумать что-то ещё!
Часть 2: шейдер исследования мира
Шейдер исследования мира (или "шейдер растворения мира", или глобальное растворение") позволяет нам одинаково скрывать все объекты сцены на основании их расстояния до позиции; сейчас мы создадим такой шейдер в Unity Shader Graph и напишем его на HLSL.
Вот пример того, что мы создадим:
Расстояние как параметр
Допустим, нам нужно растворять объект в сцене, если он слишком далеко от игрока. Мы уже объявили параметр _Amount, который управляет процессом исчезания/растворения объекта, поэтому нам нужно заменить его расстоянием между объектом и игроком.
Для этого нам нужно взять позиции Player и Object.
Позиция игрока
Процесс будет аналогичным и для Unity Shader Graph, и для HLSL: нам нужно передавать в коде позицию игрока.
private void Update()
{
//Updates the _PlayerPos variable in all the shaders
//Be aware that the parameter name has to match the one in your shaders or it wont' work
Shader.SetGlobalVector("_PlayerPos", transform.position); //"transform" is the transform of the Player
}
Shader Graph
Позиция объекта и расстояние до него
С помощью Shader Graph мы сможем использовать ноды Position и Distance.
P.S. Чтобы эта система заработала со Sprite Renderers, необходимо добавить свойство _MainTex, сэмплировать его и соединить с albedo. Можете прочитать мой предыдущий туториал Sprites diffuse shader (в котором используется shader graph).
HLSL (поверхность)
Позиция объекта
В HLSL мы можем добавить в нашу структуру Input переменную worldPos для получения позиций вершин объектов.
struct Input
{
float2 uv_MainTex;
float3 worldPos; //add this and Unity will set it automatically
};
На странице документации Unity можно узнать, какие другие встроенные параметры допустимо добавлять к структуре Input.
Применяем расстояние
Нам нужно использовать расстояние между объектами и игроком как величину растворения. Для этого можно применить встроенную функцию distance (документация Microsoft).
void surf (Input IN, inout SurfaceOutputStandard o) {
half dissolve_value = tex2D(_DissolveTexture, IN.uv_MainTex).x;
float dist = distance(_PlayerPos, IN.worldPos);
clip(dissolve_value - dist/ 6f); //"6" is the maximum distance where your object will start showing
//Set albedo, alpha, smoothness etc[...]
}
Результат (3D)
Результат (2D)
Как видите, объекты растворяются «локально», у нас не получился однородный эффект, потому что мы получаем «значение растворения» из текстуры, сэмплированной с помощью UV каждого объекта. (В 2D это менее заметно).
Трёхмерный LocalUV Dissolve Shader на HLSL
Shader "Custom/GlobalDissolveSurface" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness("Smoothness", Range(0,1)) = 0.5
_Metallic("Metallic", Range(0,1)) = 0.0
_DissolveTexture("Dissolve texture", 2D) = "white" {}
_Radius("Distance", Float) = 1 //distance where we start to reveal the objects
}
SubShader{
Tags { "RenderType" = "Opaque" }
LOD 200
Cull off //material is two sided
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
sampler2D _DissolveTexture; //texture where we get the dissolve value
struct Input
{
float2 uv_MainTex;
float3 worldPos; //Built-in world position
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
float3 _PlayerPos; //"Global Shader Variable", contains the Player Position
float _Radius;
void surf (Input IN, inout SurfaceOutputStandard o) {
half dissolve_value = tex2D(_DissolveTexture, IN.uv_MainTex).x;
float dist = distance(_PlayerPos, IN.worldPos);
clip(dissolve_value - dist/ _Radius);
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
Sprites Diffuse – LocalUV Dissolve Shader на HLSL
Shader "Custom/GlobalDissolveSprites"
{
Properties
{
[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
_Color("Tint", Color) = (1,1,1,1)
[MaterialToggle] PixelSnap("Pixel snap", Float) = 0
[HideInInspector] _RendererColor("RendererColor", Color) = (1,1,1,1)
[HideInInspector] _Flip("Flip", Vector) = (1,1,1,1)
[PerRendererData] _AlphaTex("External Alpha", 2D) = "white" {}
[PerRendererData] _EnableExternalAlpha("Enable External Alpha", Float) = 0
_DissolveTexture("Dissolve texture", 2D) = "white" {}
_Radius("Distance", Float) = 1 //distance where we start to reveal the objects
}
SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Cull Off
Lighting Off
ZWrite Off
Blend One OneMinusSrcAlpha
CGPROGRAM
#pragma surface surf Lambert vertex:vert nofog nolightmap nodynlightmap keepalpha noinstancing
#pragma multi_compile _ PIXELSNAP_ON
#pragma multi_compile _ ETC1_EXTERNAL_ALPHA
#include "UnitySprites.cginc"
struct Input
{
float2 uv_MainTex;
fixed4 color;
float3 worldPos; //Built-in world position
};
sampler2D _DissolveTexture; //texture where we get the dissolve value
float3 _PlayerPos; //"Global Shader Variable", contains the Player Position
float _Radius;
void vert(inout appdata_full v, out Input o)
{
v.vertex = UnityFlipSprite(v.vertex, _Flip);
#if defined(PIXELSNAP_ON)
v.vertex = UnityPixelSnap(v.vertex);
#endif
UNITY_INITIALIZE_OUTPUT(Input, o);
o.color = v.color * _Color * _RendererColor;
}
void surf(Input IN, inout SurfaceOutput o)
{
half dissolve_value = tex2D(_DissolveTexture, IN.uv_MainTex).x;
float dist = distance(_PlayerPos, IN.worldPos);
clip(dissolve_value - dist / _Radius);
fixed4 c = SampleSpriteTexture(IN.uv_MainTex) * IN.color;
o.Albedo = c.rgb * c.a;
o.Alpha = c.a;
}
ENDCG
}
Fallback "Transparent/VertexLit"
}
P.S. Для создания последнего шейдера я скопировал стандартный шейдер Unity Sprites-Diffuse и добавил часть с «растворением», описанный ранее в этой части статьи. Все стандартные шейдеры можно найти здесь.
Делаем эффект однородным
Чтобы сделать эффект однородным, мы можем использовать в качестве UV-координат текстуры растворения глобальные координаты (позицию в мире). Также важно задать в параметрах текстуры растворения Wrap = Repeat, чтобы мы могли повторять текстуру, не замечая этого (убедитесь, что текстура бесшовна и хорошо повторяется!)
HLSL (поверхность)
half dissolve_value = tex2D(_DissolveTexture, IN.worldPos / 4).x; //I modified the worldPos to reduce the texture size
Shader Graph
Результат (2D)
Это результат: мы можем заметить, что текстура растворения теперь однородна для всего мира.
Этот шейдер уже идеально подходит для 2D-игр, но для 3D-объектов его нужно усовершенствовать.
Проблема с 3D-объектами
Как видите, шейдер не работает для «невертикальных» граней, и сильно искажает текстуру. Так получается потому. что UV-координатам нужно значение float2, а если мы передаём worldPos, то она получает только X и Y.
Если устранить эту проблему, применив вычисления для отображения текстуры на всех гранях, то мы придём к новой проблеме: при затемнении объекты будут пересекаться друг с другом, а не останутся однородными.
Решение сложно будет понять новичкам: необходимо избавиться от текстуры, сгенерировать в мире трёхмерный шум и получать «значение растворения» из него. В этом посте я не буду объяснять генерацию 3D-шума, но можно найти кучу готовых к использованию функций!
Вот пример шейдера шума: https://github.com/keijiro/NoiseShader. Также научиться генерировать шум можно здесь: https://thebookofshaders.com/11/ и здесь: https://catlikecoding.com/unity/tutorials/noise/
Я задам свою функцию поверхности таким образом (предполагая, что вы уже написали часть с шумом):
void surf (Input IN, inout SurfaceOutputStandard o) {
float dist = distance(_PlayerPos, IN.worldPos);
//"abs" because you have to make sure that the noise is between the range [0,1]
//you can remove "abs" if your noise function returns a value between [0,1]
//also, replace "NOISE_FUNCTION_HERE" with your 3D noise function.
half dissolve_value = abs(NOISE_FUNCTION_HERE(IN.worldPos));
if (dist > _Radius) {
float clip_value = dissolve_value - ((dist - _Radius) / _Radius);
clip(clip_value);
if (clip_value < 0.05f)
o.Emission = float3(1, 1, 1);
}
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
Краткое напоминание о HLSL: прежде чем использовать/вызывать функцию, её необходимо написать/объявить.
P.S. Если вы хотите создать шейдер с помощью Unity Shader Graph, то нужно использовать Custom Nodes (и генерировать шум, написав в них код на HLSL). О Custom Nodes я расскажу в будущем туториале.
Результат (3D)
Добавление контуров
Чтобы добавить контуры, нужно повторить процесс из предыдущей части туториала.
Инвертированный эффект
А если мы захотим обратить этот эффект? (Объекты должны исчезать, если рядом находится игрок)
Нам достаточно изменить одну строку:
float dist = _Radius - distance(_PlayerPos, IN.worldPos);
(Тот же процесс относится к Shader Graph).
Комментарии (3)
Hzpriezz
24.10.2018 09:57Черт возьми, это самый полезный и доступный материал по шейдерам который можно найти в интернете, на английском есть объяснения но обычно они ограничиваются шейдер графом. Спасибо, в рамочку и на стену.
MrMureno
24.10.2018 09:58не особо информативный комментарий, но отмечу что отличный туториал и жду уже про Custom Nodes, таким же простым и понятным языком описанное с наглядными примерами)
LaG1924
Статья интересная (переводчик, как обычно, молодец). Но если в мире сложная геометрия и пиксели перекрашиваются по несколько раз, то «рисование в лоб» губит производительность. Можно ли включить что-то вроде отложенного освещения (т.е. возможность писать исходные данные, вроде мировых координат пикселя, в G-Buffer)? Или оно сразу включено, т.к. юнити вроде умеет в deffered shading и должен понимать, что в шейдерах из статьи не используется ничего особенного, чего нельзя «отложить на потом»?