Хабрахабр

[Перевод] Шейдеры растворения и исследования мира

Шейдер растворения возвращает красивый эффект, к тому же его легко создать и понять; сегодня мы сделаем его в Unity Shader Graph, а также напишем на HLSL.

Вот пример того, что мы будем создавать:

Чтобы создать шейдер растворения (dissolve shader), нам придётся работать со значением AlphaClipThreshold в шейдере «Shader Graph» или воспользоваться функцией HLSL под названием clip.

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

Можете создать и собственную — прямые, треугольники, да всё, что угодно! Просто помните, что белые части растворяются быстрее.

Эту текстуру я создал в Photoshop, воспользовавшись фильтром «Clouds».

Даже если вам интересен только Shader Graph и вы ничего не знаете о HLSL, то я всё равно рекомендую прочитать эту часть, потому что полезно понимать, как Unity Shader Graph работает внутри.

В HLSL мы используем функцию clip(x). Функция clip(x) отбрасывает все пиксели со значением меньше нуля. Поэтому если мы вызовем clip(-1), то будем уверены, что шейдер никогда не будет рендерить этот пиксель. Подробнее о clip можно прочитать в Microsoft Docs.

Свойства

Шейдеру нужны два свойства, Dissolve Texture и Amount (которая будет обозначать общий процесс выполнения). Как и в случае с другими свойствами и переменными, можно назвать их как угодно.

Properties _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"
}

Если нам нужно будет создать этот эффект с помощью Unity Shader Graph, то мы должны использовать значение AlphaClipThreshold (которое работает иначе, чем clip(x) из HLSL). В этом примере я создал PBR-шейдер.

Например, если оно равно 0. Функция AlphaClipThreshold приказывает шейдеру отбрасывать все пиксели, значение которых меньше её значения Alpha. 2f, то шейдер не будет рендерить этот пиксель. 3f, а наше значение альфы равно 0. О функции 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 логика немного отличается.

Мы можем создать очень крутые эффекты с помощью простого шейдера растворения; можно экспериментировать с различными текстурами и значениями, а также придумать что-то ещё!
Шейдер исследования мира (или "шейдер растворения мира", или глобальное растворение") позволяет нам одинаково скрывать все объекты сцены на основании их расстояния до позиции; сейчас мы создадим такой шейдер в 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://thebookofshaders.com/11/ и здесь: https://catlikecoding.com/unity/tutorials/noise/ Вот пример шейдера шума: https://github.com/keijiro/NoiseShader.

Я задам свою функцию поверхности таким образом (предполагая, что вы уже написали часть с шумом):

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: прежде чем использовать/вызывать функцию, её необходимо написать/объявить.

S. P. О Custom Nodes я расскажу в будущем туториале. Если вы хотите создать шейдер с помощью Unity Shader Graph, то нужно использовать Custom Nodes (и генерировать шум, написав в них код на HLSL).

Результат (3D)

Добавление контуров

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

Инвертированный эффект

А если мы захотим обратить этот эффект? (Объекты должны исчезать, если рядом находится игрок)

Нам достаточно изменить одну строку:

float dist = _Radius - distance(_PlayerPos, IN.worldPos);

(Тот же процесс относится к Shader Graph).

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»