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

от автора

Часть 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).


ссылка на оригинал статьи https://habr.com/post/427401/


Комментарии

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

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