Основы программирования шейдеров. #1 Краткая теория

Содержание:

Vertex and Fragment Shader

В этой статье мы напишем простой вертексно-пиксельный шейдер, который
будет натягивать текстуру(логотип XGM) на объект(в моем случае на
сферу).
Итак, приступим.
Для начала создадим папку, где будем хранить шейдеры. Назовем ее
Shaders. Далее создадим шейдер: правый клик по папке
Shaders>Create>Shader>Unlit Shader. Назовем шейдер xgmSkin.
Перед нами открылось окно редактора с содержанием стандартного Unlit
Shader'а. Выделяем все и смело удаляем. Начнем, как говориться, "с чистого листа".
Из предыдущей статьи мы уяснили, что в Unity шейдер пишется внутри
SubShader'а. Так-что давайте сначала напишем "фундамент".
Shader "MyShaders/xgmSkin" // путь и имя шейдера в выпадающем окне 
материала
{
  Properties // отображаемые в инспекторе свойства
   { 
    
   }
   SubShader 
   {
     Pass 
      {
        CGPROGRAM
        // тут будет CG код
        CGEND
      }
   }
  Fallback "Diffuse" 
}
Начнем заполнять блок Properties. В нем мы обозначаем внешние
переменные, которые будут видны в окне материала в инспекторе. Здесь нам
многого не нужно, только текстурка. Пишем:
  Properties 
  {
    _xgmTex("Texture",2D) = "white" {} 
  }
Ну и всё, никаких тэгов писать не будем, нам подходят настройки по
дефолту, так что сразу спускаемся в блок CGPROGRAM.
В статье по теории я писал, что мы можем назначать, какие данные примет и
вернет шейдер при помощи семантики. Для удобства записи в CG программе
эти данные объединяют в структуры. Давайте опишем входящую(vertexInput) и
выходящую(vertexOutput) структуры для вертексного шейдера:
struct vertexInput // входящие данные
      {
        float4 vertex:POSITION; // позиция вершины в пространстве модели
        float2 uv:TEXCOORD0; // uv координаты данной вершины 
  
      };
      struct vertexOutput // выходящие данные
      {
       float4 position:SV_POSITION; // позиция 
       float2 uv:TEXCOORD0; // uv координаты
      };
После структур обязательно ставится знак ";"
Данные описаны, теперь самое время писать векртексный шейдер. Ой, чуть не забыл)
Для того, что бы обозначить функцию как вертексный или пиксельный
шейдер, мы должны назначить имена для этих функций в директиве #pragma.
#pragma vertex vert 
#pragma fragmet frag 
А вот собственно сам вертексный шейдер:
vertexOutput vert(vertexInput v) 
      {
        vertexOutput o; // создаем возвращаемую структуру 
        o.position = mul(UNITY_MATRIX_MVP,poss); // переводим координатыиз пространства модели в проекционное 
        o.uv = v.uv; // просто передаем uv координаты
        return o; 
      }
Пиксельный шейдер возвращает только цвет, так что нужды создавать
отдельную структуру для него у нас нет. Поэтому,назначаем семантику
прямо для самого возвращаемого значения функции frag(Семантика для цвета
COLOR или SV_Target ). Так же нам нужно вытащить текстуру из
Properties.Напомню, что имя внешней переменной должно быть такое-же, как в
Properties, что бы движок сам передал ей значение.
      uniform sampler2D _xgmTex; // внешняя текстура 

      fixed4 frag(vertexOutput v):SV_Target 
      {
        fixed4 col = tex2D(_xgmTex,v.uv);// берем цвет из текстуры по UV
 координатам
        return col;
      } 
Ну вот и всё, наш шейдер готов!
Полный код шейдера
Shader "MyShaders/xgmSkin" 
{
  Properties 
  {
    _xgmTex("Texture",2D) = "white" {} 
  }

SubShader
 {
   Pass
   {
     CGPROGRAM 
     #pragma vertex vert // говорим имя у вертексного шейдера 
     #pragma fragment frag  // говорим имя пиксельного шейдера

      struct vertexInput 
      {
        float4 vertex:POSITION;
        float2 uv:TEXCOORD0;
      };
      struct vertexOutput
      {
       float4 position:SV_POSITION;
       float2 uv:TEXCOORD0;
      };
      vertexOutput vert(vertexInput v) 
      {
        vertexOutput o; // возвращаемая структура 
        o.position = mul(UNITY_MATRIX_MVP,v.vertex); // переводим координаты
        из пространства модели в проекционное 
        o.uv = v.uv; // просто передаем uv координаты
        return o;
      }
      uniform sampler2D _xgmTex; // внешняя текстура 

      fixed4 frag(vertexOutput v):SV_Target
      {
        fixed4 col = tex2D(_xgmTex,v.uv); // берем цвет из текстуры по 
UV координатам
        return col;
      } 
     ENDCG  
   } 
  } 
  Fallback "Diffuse"
 } 
Пора применить его на объекте! Для того что-бы применить шейдер для
какого-либо объекта, нужно создать новый материал и назначить наш шейдер
в выпадающем окне материала(если вы не забыли, наш шейдер находится в MyShaders>xgmSkin)
Теперь выберем нужную текстуру в окне материала.
Вуаля!
Теперь,думаю,стоит немного усложнить наш шейдер.Добавим в блок
Properties цвет, что-бы иметь возможность вручную управлять цветом.
    _Color ("LogoColor",Color) = (0,0,0,0)
И объявим его внутри шейдера в том-же месте, где объявляли текстуру
 uniform fixed4 _Color;
Надеюсь,вы читали первую статью, ну вот ту часть про цвет ? В общем сейчас мы
хотим поколдовать с цветом.
В итоге я изменил одну строчку в пиксельном шейдере
      fixed4 frag(vertexOutput v):SV_Target
      {
        fixed4 col = tex2D(_xgmTex,v.uv); // берем цвет из текстуры по 
UV координатам
        return (1-col)*_Color;
      } 
1=(1,1,1,1); 0 = (0,0,0,0)
В итоге,если мы установим _Color на зеленый цвет, у нас получилась
такая вот сфера, у которой мы можем менять цвет текстуры логотипа по
нашему желанию.)
Итак, теперь мы, вроде, умеем делать простые-препростые шейдеры. Давайте
капнем немного глубже и сделаем нашей сфере дифузное освещение.
Вот формула, по которой рассчитывается освещенность для точки:
dif = max(0,(n*l)), где n - нормаль, l - направление света.
Заранее скажу, что рассчитывать освещение мы будет в мировых
координатах, следовательно, нормаль, которая дается в объектных
координатах придется переводить в мировые. Обычная матрица, которую мы
используем для частиц тут не подойдет, так-что для таких случаев
припасена встроенная инверсная матрица _World2Object.
Так-же хочу сказать о том, что в ShaderLab есть встроенная переменная
_WorldSpaceLightPos0. По названию можно понять, что это вектор, который
содержит мировые координаты источника света, но тут есть одна хитрость.
Если на карте используется только 1 источник света ( Direction light),
то тогда в эту переменную записывается вектор освещения, а если больше,
то ,действительно, положение источника света. В данном случае у нас один
источник, так что эта переменная нам подходит.
Еще кое-что.Как мы все знаем, семантика TEXCOORD используется для передачи
uv координат. Но, т.к. их может быть несколько штук, например,
TEXCOORD1,TEXCOORD2,TEXCOORD3..., ее удобно использовать, что-бы
передать нужные данные из вертексного шейдера в пиксельный, т.к. у
переменных во входящих/выходящих струтурах обязательно должна семантика.
Ну вот теперь вроде всё. Поехали!
Добавим в vertexInput строчку, которая будет запрашивать нормаль:
float3 norm:NORMAL;
И в vertexOutput переменную для хранения мировых координат нормали:
       float3 worldNormal:TEXCOORD1;
Далее идем в вертексный шейдер и переводим нормаль из объектного в
мировые координаты:
        o.worldNormal = mul(_World2Object,v.norm);
Ну и рассчитаем силу освещения в пиксельном шейдере:
      fixed4 frag(vertexOutput v):SV_Target
      {
        fixed4 col = tex2D(_xgmTex,v.uv); // берем цвет из текстуры по UV координатам
        float3 l = normalize(_WorldSpaceLightPos0); // нормализуем вектор освещения
        float3 n = normalize(v.worldNormal); // нормализуем вектор нормали 
        float dif = max(0.0,dot(n,l)); // рассчитываем освещенность пикселя 
        return ((1-col)*_Color)*dif; 
      } 
Ну вот как-то так. Теперь, если мы перейдем в юнити и изменим
направление лучей у Direction light'а, то заметим, что участки с
противоположной стороны от света затеняться.
Полный код шейдера
Shader "MyShaders/xgmSkin"
{
Properties
{
_xgmTex("Texture",2D) = "white" {}
_Color ("LogoColor",Color) = (0,0,0,0)
}
SubShader
{
Pass
{
Cull Off
CGPROGRAM
#pragma vertex vert говорим имя у вертексного шейдера
#pragma fragment frag говорим имя пиксельного шейдера
struct vertexInput
{
float4 vertex:POSITION;
float2 uv:TEXCOORD0;
float3 norm:NORMAL;
};
struct vertexOutput
{
float4 position:SV_POSITION;
float2 uv:TEXCOORD0;
float3 worldNormal:TEXCOORD1;
};
vertexOutput vert(vertexInput v)
{
vertexOutput o; возвращаемая структура
o.position = mul(UNITY_MATRIX_MVP,v.vertex); переводим координаты из пространства модели в проекционное
o.uv = v.uv; просто передаем uv координаты
o.worldNormal = mul(_World2Object,v.norm); переводим нормаль в мировые к-ты
return o;
}
uniform sampler2D _xgmTex; внешняя текстура
uniform fixed4 _Color;
fixed4 frag(vertexOutput v):SV_Target
{
fixed4 col = tex2D(_xgmTex,v.uv); берем цвет из текстуры по UV координатам
float3 l = normalize(_WorldSpaceLightPos0); нормализуем вектор освещения
float3 n = normalize(v.worldNormal); нормализуем вектор нормали
float dif = max(0.0,dot(n,l)); рассчитываем освещенность пикселя
return ((1-col)*_Color)*dif;
}
ENDCG
}
}
Fallback "Diffuse"
}
Итог:
To be continued ...
P.S. спонсоры данных статьей - кофе и свободное время. Спасибо им за то
что они есть.

`
ОЖИДАНИЕ РЕКЛАМЫ...