Surface Shader

Surface Shader



Вступление 

Думаю мало кто не согласится с тем, что написание вертексных и пиксельных шейдеров работа достаточно трудоёмкая. Для того, чтобы написать простой шейдер с поддержкой карты нормалей и бликов, придется вручную написать  400 строк кода. И всё потому, что реализация содержит множество технических аспектов, которые нужно учитывать (forward/deffered lighting, множественные проходы, различные матрицы переходов и тд). Поэтому разработчики движка создали упрощенный вид шейдера - поверхностный, который выполняет все те-же действия за кулисами, позволяя нам добиваться нужных эффектов без столь громоздкого кода.

Немного теории 

!Начнем! с того, что это хоть и упрощенный, но всё-таки шейдер, следовательно, его мы пишем на языке *CG* внутри блока CGPROGRAM...ENDCG, который, в свою очередь, находится внутри блока SubShader{}, но в отличие от вертексных/пиксельных шейдеров, поверхностный не нужно помещать внутрь блока прохода *Pass{}*.

Вот как выглядит шаблон поверхностного шейдера:

 Shader "MyShaders/ExampleShader"

{

 Properties

{...}

SubShader

{

 CGPROGRAM 

#pragma surface surfaceFunction lightModel

struct Input

{

...

}; 

void surfaceFunction (Input IN, inout SurfaceOutput o)

{...}

ENDCG

}

Fallback "Diffuse"

}

!Разберем! все по порядку:

#pragma surface surfaceFunction .. - идентификация поверхностного шейдера.

lightModel - указываем, какую модель освещения мы будем использовать.

Другими словами, строчка #pragma surface surf Lambert говорит компилятору, что функция surfaceFunction - это поверхностный шейдер, который использует модель освещения

lightModel. // Встроенные в юнити модели освещения: Lambert, BlinnPhong, Standard.

struct Intput{} - задаем структуру с входными данными

void surfaceFunction (Input IN, inout SurfaceOutput o){} - собственно, сама шейдерная функция. На вход принимает входную структуру Input IN. Внутри функции шейдера мы производим вычисления и заполняем все поля специальной выходной структуры SurfaceOutput o.

Входная структура 

!Входная! структура в поверхностном шейдере может содержать следующие переменные:

float3 viewDir - содержит направление взгляда

float4 SomeName : COLOR - содержит интерполированный цвет

float4 screenPos - содержит экранные координаты

float3 worldPos - содержит мировые координаты точки

float3 worldRefl - содержит направление отражения вектора взгляда от данной точки в мировых координатах //(если не задана карта нормалей)

float3 worldNormal - содержит направление вектора нормали в мировых координатах // (если не задана карта нормалей)

float3 worldRefl; INTERNAL_DATA - содержит направление отражения вектора взгляда от данной точки в мировых координатах //(если задана карта нормалей, используется вместе с функцией WorldReflectionVector (IN, o.Normal) )

float3 worldNormal; INTERNAL_DATA - содержит направление вектора нормали в мировых координатах // (если задана карта нормалей, используется вместе с функцией WorldNormalVector (IN, o.Normal) )

Чтобы задать uv координаты, нужно подписать префикс uv_ к имени названию текстуры. Например, float 2 uv_mainTexture

Выходная структура 

!Выходная! структура содержит в себе параметры поверхности, которые мы можем заполнить в поверхностном шейдере. Юнити предлагает нам две структуры на выбор:

Стандартная, используется для моделей освещения Lambert'а и BlinnPhonga'а.

struct SurfaceOutput

{

  fixed3 Albedo; // цвет

  fixed3 Normal; // нормаль в пространстве касательных ( из карты нормалей) 

  fixed3 Emission; // свечение

  half Specular; // сила спекуляра в пределах от 0 до 1

  fixed Gloss;  // интенсивности спеуляра

  fixed Alpha;  // прозрачность

};

Более физически корректные, используются для моделей Standard


struct SurfaceOutputStandard

{

  fixed3 Albedo;   // цвет

  fixed3 Normal;   // нормаль в пространстве касательных ( из карты нормалей)

  half3 Emission; // свечение

  half Metallic;   // 0 - не метал, 1 - метал

  half Smoothness;  // 0 - грубая поверхность, 1 - гладкая поверхность

  half Occlusion;   // влияние на цвет окружения

  fixed Alpha;    // прозрачность

};

Дополнительные модификации 

-

!Дополнительные! модификации задаются в директиве препроцессора после указания модели освещения.

Кастомные функции === 

vertex:vertexFunction - функция для изменения вертексного шейдера

finalcolor:ColorFunction - функция для изменения финального цвета

finalgbuffer:ColorFunction - функция для изменения контента G-буффера в отсроченном освещении

Тени и тесселяция == 

fullforwardshadows - добавляет тени для всех источников света в Forward rendering освещении

tessellate:TessFunction - функция, которая вычисляет параметры тесселяции. (для работы требует DX11или выше)

Функции генерации кода == 

!Обычно! поверхностный шейдер реализует все возможные сценарии освещения/затенения/лайтмаппинга. Однако, в некоторых случаях это нам совсем не нужно. Для отключения ненужных опций шейдера существуют специальные команды. Вот некоторые из них:

exclude_path:deferred, exclude_path:forward, exclude_path:prepass - отключение генерации прохода модели освещения

noshadow - отключение всех теней

noambient - отключение влияния ambient lighting или light probes

nolightmap - отлючение всех лайтмапов

noforwardadd - отключение добавочного прохода в forward rendering

Пишем шейдер 

!Ну! вот и всё, пока нам этих знаний должно хватить. Настал тот самый момент, когда мы можем сделать свой велосипед применить полученные знания на практике.

Создадим новый поверхностный шейдер: Create->Shader->Standard Surface Shader

Я назову его "FirstSurface"

Делаем двойной клик по шейдеру и смотрим, что у нас тут:

!Да!, собственно, ничего особенного. Этот шейдер берет значения из инспектора и прямиком транслирует их в шейдерную функцию. Думаю, стоит сделать из него что-нибудь более интересное. Задумка состоит в том, чтобы мы сделалии ледяную сферу с трещинами, в которых будем время от времени проявляться лава.

Ингредиенты:

* Сфера

* Текстура льда

* Текстура лавы

* Наспех сделанная карта нормалей льда



Начнем модифицировать шейдер. Сперва добавим в окно свойств недостающие переменные:


Properties 

{

_Color ("Color", Color) = (1,1,1,1)

_MainTex ("Albedo (RGB)", 2D) = "white" {} // текстура льда

_NormalMap("NormalMap",2D) = "white"{} // карта нормалей льда

_SecondTex ("Lava_Texture",2D) = "white"{} // текстура лавы

_Glossiness ("Smoothness", Range(0,1)) = 0.5

_Metallic ("Metallic", Range(0,1)) = 0.0

_Str ("Strenght", Range(0,10)) = 1 // переменная для регулировки силы

}

Следующим нашим действием будет добавление объявление внешних переменных из окна свойств

sampler2D _NormalMap;
sampler2D _SecondTex;
half _Str;

и добавлением переменных с uv координатами в входную структуру Input

struct Input 
  {
   float2 uv_MainTex;
   float2 uv_NormalMap;
   float2 uv2_SecondTex;
  };

заметьте, что к внешним переменным не обязательно обозначать как !uniform!, хотя, по канону, это желательно делатьВот мы и добрались до самой шейдерной функции. Первым делом назначим нашу карту нормалей:


o.Normal = UnpackNormal(tex2D(_NormalMap,IN.uv_NormalMap));
float3 norm = o.Normal;

Затем создадим переменные для льда и лавы:


fixed4 lava = tex2D(_SecondTex,(IN.uv2_SecondTex));
fixed4 ice = tex2D(_MainTex,IN.uv_MainTex);

Теперь нам стоит подумать как добиться, чтобы лава была только в трещинах. Какая переменная у нас отвечает за высоту поверхности ? Правильно, нормаль. Путем плясок с бубном было выяснено, что за трещины отвечает xy компонента вектора нормали. Интерполируем цвет между цветом льда и красным с использованием длинны norm.xy:


fixed4 col = lerp(ice,fixed4(1,0,0,0),length(norm.xy)); 

Вот что у нас получилось:

Как-то слабовато. Умножим length(norm.xy) на _Str=2 и посмотрим что получиться:

Уже намного лучше. Заменим красный цвет на лаву:


fixed4 col = lerp(ice,lava,length(norm.xy)*_Str);

Ну, и последний штрих - добавить мерцание. Здесь я буду использовать переменную _SinTime.w, т.к. она изменяется в пределах от 0 до 1, что и требуется. Еще немного плясок с бубном и вот что я вывел:


fixed4 c = lerp(ice * _Color,max(lava-0.5,lava*(_SinTime.w+_CosTime.x)*length(norm.xy)*_Str),length(norm.xy)*_Str);

При желании, можно поиграться со значениями и формулой, чтобы получить более интересный результат.

Что получилось у меня:

https://youtu.be/O0U0MoRFILw


Report Page