Unity
October 2, 2020

Unity Simple Snow Shader

Если вы новичок в написании шейдеров, эта статья поможет вам разобраться в:

  • Как создать шейдер снежного покрова на объектах
  • Как создать Normal для шейдера
  • Изменение текстуры, примененной к пикселям
  • Изменение поверхности вершин модели

Чертёж шейдера

Итак, что мы хотим сделать, мы можем сказать это простыми словами:

По мере увеличения уровня снега мы хотим превратить пиксели, которые направленные по оси к направлению снега, а цвет, в цвет снега, а не в текстуру материала.

По мере увеличения уровня снега мы хотим немного деформировать модель, чтобы она стала больше, преимущественно со стороны, с которой идет снег.

Шаг 1 – Bumped Diffuse Shader

Итак, давайте начнем с нового диффузного шейдера и добавим рельефное отображение.

Shader "Custom/SnowShader"

{

Properties {

_MainTex ("Base (RGB)", 2D) = "white" {}

//New normal map texture

_Bump ("Bump", 2D) = "bump" {}

}

SubShader {

Tags { "RenderType"="Opaque" }

LOD 200

CGPROGRAM

#pragma surface surf Lambert

sampler2D _MainTex;

//Must add a sample with the same name

sampler2D _Bump;

struct Input {

float2 uv_MainTex;

//Get the uv coordinates for the bump map

float2 uv_Bump;

};

void surf (Input IN, inout SurfaceOutput o) {

half4 c = tex2D (_MainTex, IN.uv_MainTex);

//Extract the normal map information from the texture

o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump);

o.Albedo = c.rgb;

o.Alpha = c.a;

}

ENDCG

}

FallBack "Diffuse"

}

Это практически тот шейдер, который Unity автоматически делает для нас в качестве основы, просто добавляя Bump.

Итак, мы:

  • Определили свойство под названием _Bump, которое представляет собой 2D-изображение со значением по умолчанию «bump» (пустая карта нормалей).
  • Создали sampler2D с точно таким же именем
  • Создали запись в Input, чтобы получить uv-координаты для Bump (снова с тем же именем)
  • Добавили ​​строку кода, вызывающая функцию UnpackNormal, которая берет текстуру карты нормалей и преобразует результат в нормаль - мы передаем ей пиксель из текстуры, используя tex2D, переменную _Bump и координаты uv из структуры Input

После этого у нас есть довольно ничем не примечательный шейдер с рельефом.

Шаг 2 - Добавляем снег

Хорошо, для этого шага нам нужно выяснить, направлена ​​ли нормаль пикселей примерно в том же направлении, что и нормали снега.

Мы собираемся сделать это с помощью скалярного произведения. Скалярное произведение между двумя векторами единичной длины равно косинусу угла между этими векторами. Полезно, что в CG есть точечная функция, которая рассчитает это за нас. Самое замечательное в скалярном произведении заключается в том, что оно полезно, когда 1 вектор указывают точное направление , и -1, когда они указывают в точно противоположных направлениях, с хорошим линейным масштабом между ними. Таким образом, нам никогда не нужно знать угол для нашего шейдера, только точку нормали пикселей и направление снега.

Единичный вектор - это вектор, величина которого равна 1, поэтому квадратный корень из квадратов его компонентов x, y и z должен быть 1. Не попадайтесь в ловушку, думая, что вектор вроде (1,1,1) является Единичный вектор - это не так. Чтобы найти угол, вы должны масштабировать любые векторы, длина которых больше единицы, так, чтобы они стали единичной длиной.

Нам нужно определить некоторые свойства для нашего шейдера.

Properties

{

_MainTex ("Base (RGB)", 2D) = "white" {}

_Bump ("Bump", 2D) = "bump" {}

_Snow ("Snow Level", Range(0,1) ) = 0

_SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)

_SnowDirection ("Snow Direction", Vector) = (0,1,0)

_SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1

}

Мы создали:

  • переменную Snow, которая будет количеством снега, покрывающего предмет, которая всегда находится в диапазоне 0–1.
  • цвет нашего снега (избегайте желтого), который по умолчанию белый
  • направление, с которого падает снег (по умолчанию он падает прямо вниз, поэтому наш вектор перекрытия направлен прямо вверх)
  • глубина нашего снега, которую мы будем использовать при изменении вершин на шаге 3, которая находится в диапазоне 0..0,3

Следуя информации в № 1 этой серии - теперь мы идем и проверяем, у нас есть переменные с правильными именами:

sampler2D _MainTex;

sampler2D _Bump;

float _Snow;

float4 _SnowColor;

float4 _SnowDirection;

float _SnowDepth;

Обратите внимание, мы можем рассматривать это все как флоат переменную разных размеров, кроме сэмплеров текстуры. Помните, что раздел Cg между CGPROGRAM и ENDCG фактически не зависит от остальной части шейдера, использующего ShaderLab, который является шейдерной системой Unity. Свойства, определенные в разделе «Properties», принадлежат ShaderLab и должны быть связаны с Cg. Это причина, по которой нам нужно объявить их снова с тем же именем, компилятор ShaderLab сделает ссылку.

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

Этот бит требует небольшого чтения в документации - в основном потому, что мы хотим писать в o.Normal в нашем шейдере, нам нужно получить INTERNAL_DATA, предоставленные Unity, а затем вызвать функцию WorldNormalVector в нашей программе шейдера, которой нужна эта информация. Практический результат в том, что нам нужно поместить эти вещи в структуру Input.

struct Input

{

float2 uv_MainTex;

float2 uv_Bump;

float3 worldNormal;

INTERNAL_DATA

};

Теперь мы наконец можем написать нашу шейдерную программу.

void surf (Input IN, inout SurfaceOutput o) {

//Normal color of a pixel

half4 c = tex2D (_MainTex, IN.uv_MainTex);

//Get the normal from the bump map

o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

//Get the dot product of the real normal vector and our snow direction

//and compare it to the snow level

if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) > lerp(1,-1,_Snow))

//If this should be snow pass on the snow color

o.Albedo = _SnowColor.rgb;

else

o.Albedo = c.rgb;

o.Alpha = 1;

}

Итак, мы бы хотели проанализировать оператор if, в котором происходит вся магия:

Итак, мы собираемся получить скалярное произведение двух векторов - один - это направление нашего снега, а другой - вектор, который будет фактически использоваться для нормали пикселя - комбинации мировой нормали для этой точки и карты рельефа. Мы получаем эту нормаль, вызывая WorldNormalVector, передавая ему структуру Input с нашими новыми INTERNAL_DATA и нормалью пикселя из карты рельефа. После этого скалярного произведения у нас будет значение от 1 ( точный пиксель в направлении снега) и - 1 (как раз наоборот)

Затем мы сравниваем значение точки с Lerp - если наш уровень снега равен нулю (снега нет), он возвращает 1, а если уровень снега равен 1, он возвращает -1 (весь объект покрыт). При использовании этого шейдера вполне нормально изменять уровень снега только в диапазоне 0..0,5, чтобы снег оставался только на поверхностях, которые фактически обращены в сторону снега.

Когда точка больше, чем уровень снега, мы используем цвет снега, в противном случае мы используем текстуру

Теперь это полностью рабочий шейдер снега и выглядит так:

Shader "Custom/SnowShader" {

Properties {

_MainTex ("Base (RGB)", 2D) = "white" {}

_Bump ("Bump", 2D) = "bump" {}

_Snow ("Snow Level", Range(0,1) ) = 0

_SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)

_SnowDirection ("Snow Direction", Vector) = (0,1,0)

_SnowDepth ("Snow Depth", Range(0,3)) = 0.1

}

SubShader {

Tags { "RenderType"="Opaque" }

LOD 200

CGPROGRAM

#pragma surface surf Lambert

sampler2D _MainTex;

sampler2D _Bump;

float _Snow;

float4 _SnowColor;

float4 _SnowDirection;

float _SnowDepth;

struct Input {

float2 uv_MainTex;

float2 uv_Bump;

INTERNAL_DATA

};

void surf (Input IN, inout SurfaceOutput o) {

//Normal color of a pixel

half4 c = tex2D (_MainTex, IN.uv_MainTex);

//Get the normal from the bump map

o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

//Get the dot product of the real normal vector and our snow direction

//and compare it to the snow level

if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>=lerp(1,-1,_Snow))

//If this should be snow pass on the snow color

o.Albedo = _SnowColor.rgb;

else

o.Albedo = c.rgb;

o.Alpha = 1;

}

ENDCG

}

FallBack "Diffuse"

}

Деформация модели

Последний шаг - нам нужно деформировать модель, чтобы сделать ее больше, преимущественно (но не полностью) в направлении снега.

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

#pragma surface surf Lambert vertex: vert

В конце прагмы мы добавляем параметр vertex, который предоставляет имя нашей вершинной функции: vert.

Теперь наша вершинная функция выглядит так:

void vert (inout appdata_full v)

{

//Convert the normal to world coordinates

float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);

if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3)){

v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;

}

}

Сначала мы передаем ему параметр - это входящие данные, и мы решили использовать appdata_full (из Unity), который имеет координаты текстуры, нормаль, положение вершины и касательную. Вы можете передать дополнительную информацию своей пиксельной функции, указав второй параметр с вашей собственной структурой входных данных - куда вы можете добавить дополнительные значения, если хотите - нам это не нужно. Направление снега находится в мировом пространстве, но мы работаем в пространстве объекта (координаты модели), поэтому нам нужно перенести направление снега в пространство объекта, умножив его на матрицу, поставляемую Unity, которая предназначена для этой цели. Теперь у нас есть только нормаль вершины, поэтому мы делаем те же вычисления для направления снега, что и раньше, но мы масштабируем уровень снега на 2/3, так что изменяются только области, хорошо покрытые снегом. Предполагая, что наш тест пройден, мы модифицируем вершину, умножая ее нормаль + наше текущее направление на коэффициент глубины и текущий уровень снега. Это приводит к тому, что вершины перемещаются больше в направлении снега и увеличивают это искажение по мере увеличения уровня снега. Вот и все - наша работа сделана!

Shader "Custom/SnowShader" {

Properties {

_MainTex ("Base (RGB)", 2D) = "white" {}

_Bump ("Bump", 2D) = "bump" {}

_Snow ("Snow Level", Range(0,1) ) = 0

_SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)

_SnowDirection ("Snow Direction", Vector) = (0,1,0)

_SnowDepth ("Snow Depth", Range(0,0.2)) = 0.1

}

SubShader {

Tags { "RenderType"="Opaque" }

LOD 200

CGPROGRAM

#pragma surface surf Lambert vertex:vert

sampler2D _MainTex;

sampler2D _Bump;

float _Snow;

float4 _SnowColor;

float4 _SnowDirection;

float _SnowDepth;

struct Input {

float2 uv_MainTex;

float2 uv_Bump;

float3 worldNormal;

INTERNAL_DATA

};

void vert (inout appdata_full v) {

//Convert the normal to world coordinates

float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);

if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3)){

v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;

}

}

void surf (Input IN, inout SurfaceOutput o) {

half4 c = tex2D (_MainTex, IN.uv_MainTex);

o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>=lerp(1,-1,_Snow))

o.Albedo = _SnowColor.rgb;

else

o.Albedo = c.rgb;

o.Alpha = 1;

}

ENDCG

}

FallBack "Diffuse"

}

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

Реализация шейдера

Таким образом, большая разница с этим шейдером заключается в том, что мы переходим от двоичного переключателя к диапазону значений. Это заставляет нас переписать логику шейдера и начать использовать математику, а не переключатели на основе if.

Сначала нам нужно свойство, чтобы указать, насколько мы должны растушевать снег - мы назовем это _Wetness, чтобы не было лучшего названия:

_Wetness ("Wetness", Range(0, 0.5)) = 0.3

Затем нам понадобится переменная для представления свойства:

float _Wetness;

Теперь мы вычисляем разницу между точками пикселей, нормалями по отношению к направлению снега, с текущим значением lerped на основе уровня снега. Это дает нам значение в виде косинуса угла, которое также представляет _Wetness.

void surf (Input IN, inout SurfaceOutput o) {

half4 c = tex2D (_MainTex, IN.uv_MainTex);

o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

float difference = dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) - lerp(1,-1,_Snow);;

difference = saturate(difference / _Wetness);

o.Albedo = difference*_SnowColor.rgb + (1-difference) *c;

o.Alpha = c.a;

}

Затем мы насыщаем разницу между нормалью пикселя и диапазоном нашего текущего снега, деленным на _Wetness.

Насыщение дает нам значение, ограниченное от 0 до 1.

Таким образом, если мы вышли за пределы допустимого диапазона заснеженности (разница была 0), значение будет равно 0. Если _Wetness было по умолчанию 0,3, если мы были в пределах 27 градусов (30% от 90 градусов), тогда значение будет где-то между 0 ..1, иначе значение будет 1. Диапазон косинуса от 1 до -1 - разница в 2, это представляет 180 градусов (то же направление в противоположном направлении), следовательно, значение 1 в единицах косинуса составляет 90 градусов. . Наш расчет 0,3 * 90 = 27 градусов. Затем мы берем эту разницу и умножаем ее на цвет снега - получая пропорцию этого цвета, мы затем берем обратную пропорцию цвета текстуры и складываем их вместе. Это эффективно смешивает цвет снега с цветом текстуры под углом 27 градусов между началом снега и становится полностью непрозрачным. Исправление вершин Наша единственная проблема сейчас заключается в том, что если снег очень насичен, наша модель может расшириться до того, как станет снежным! Это не очень реалистично. Таким образом, мы применяем наш коэффициент влажности к диапазону снега, это означает, что модель будет расширяться позже в зависимости от того, насколько он насыщен.

void vert (inout appdata_full v) {

if(dot(v.normal, _SnowDirection.xyz) >= lerp(1,-1, ((1-_Wetness) * _Snow*2)/3)){

v.vertex.xyz += (_SnowDirection.xyz + v.normal) * _SnowDepth * _Snow;

}

}

Таким образом, модификация заключается в масштабировании уровня _Snow на 1 - _Wetness - это означает, что при 0 насыщенности ничего не меняется, а при нашей полной (0,5) насыщенности мы эффективно масштабируем коэффициент снега на 1/3, а не на 2/3 - делая модель доработать на 50% позже.

Вот и все, работа сделана.

Shader "Custom/SnowShader" {

Properties {

_MainTex ("Base (RGB)", 2D) = "white" {}

_Bump ("Bump", 2D) = "bump" {}

_Snow ("Snow Level", Range(0,1) ) = 0

_SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)

_SnowDirection ("Snow Direction", Vector) = (0,1,0)

_SnowDepth ("Snow Depth", Range(0,0.2)) = 0.1

_Wetness ("Wetness", Range(0, 0.5)) = 0.3

}

SubShader {

Tags { "RenderType"="Opaque" }

LOD 200

CGPROGRAM

#pragma surface surf Lambert vertex:vert

sampler2D _MainTex;

sampler2D _Bump;

float _Snow;

float4 _SnowColor;

float4 _SnowDirection;

float _SnowDepth;

float _Wetness;

struct Input {

float2 uv_MainTex;

float2 uv_Bump;

float3 worldNormal;

INTERNAL_DATA

};

void vert (inout appdata_full v) {

//Convert the normal to world coortinates

float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);

if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3)){

v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;

}

}

void surf (Input IN, inout SurfaceOutput o) {

half4 c = tex2D (_MainTex, IN.uv_MainTex);

o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

half difference = dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) - lerp(1,-1,_Snow);;

difference = saturate(difference / _Wetness);

o.Albedo = difference*_SnowColor.rgb + (1-difference) *c;

o.Alpha = c.a;

}

ENDCG

}

FallBack "Diffuse"

}

ORIGINAL WEB SITE HERE