alexprey alexprey

Добавил скриншоты на главную, позже загружу дневники разработки игры, где можно будет посмотреть с чего все начиналось :)

alexprey alexprey

EfReeZe, о да, тогда было знатное время и игра казалась сделано круто :D До сих пор иногда играю на телефоне от скуки)

EfReeZe EfReeZe

Это был наш далёкий 2014...

Jusper Jusper

Запись трансляции на YouTube

Jusper Jusper

Случайно сделали битву сварщиков
https://devtribe.ru/p/slash-polygon

Jusper Jusper

Внезапно на сайте потерялся 15-й выпуск. Перевыложили.

Jusper Jusper

Я вам еще не закончил писать обратную связь по боевке, есть много вещей которые немного смутили, но они поправимы. Завтра, если перестанут дергать - чиркану. Спасибо за демку!

Jusper Jusper

ruggaraxe,

Да, в этом плане все ок. Логично, что графен на старой машине, если не упарываться, не взлетит. Но я рад, что это было не 5 фпс, как даже в некоторых АА (типа Pillars of Eternity в некоторых схватках...

Jusper Jusper

ruggaraxe,

Подкреплю ее к публикации.

ruggaraxe ruggaraxe

Jusper, вот ссылка на анкету (я затупил со ссылкой с топике, сорри)
https://docs.google.com/forms/d/e/1FAIpQLSd_Wn53lJFrnfGpWI2IX...

ruggaraxe ruggaraxe

Jusper, честно говоря, да на 800х600 даже не проверяли... :) сорри. Ориентировались на FullHD и выше. Хотя над интерфейсом конечно же надо еще хорошенько поработать.
Тултипы постараемся сделать обязательно к следующей версии...

GenElCon GenElCon

Jusper,

Наверное. В прошлом они сделали Endless Legend - посмотри и сразу станет ясно в какую сторону они работают.

Jusper Jusper

GenElCon,

Я не очень понял по трейлеру геймплей. Это что-то типа цивы? Или это RTS?

GenElCon GenElCon

Humankind от разработчиков Endless Legends (и Space, но тут важно именно Legends).
А также согревающие сердца олдов трейлеры Port Royal 4 и Knights of Honor.

Jusper Jusper

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

Jusper Jusper

Первое, оно же самое тяжелое - UI. Я конечно, понимаю, что 800x600 совсем уже не в моде (завтра проверю на нормальной широформатной машине). Заблюренный текст я еще прочитать могу, но вот конкретно размер его крайне мал...

...
Jusper Jusper

ruggaraxe, я поиграл на старом маке 2012 года (Macbook Pro, Intel HD 4000), рад что с учетом довольно нагруженной по свету и теням картинке игруля не лагает как последняя сволочь (лагает конечно, но очень терпимо...

Логотип проекта Unity

#4 Vertex and Fragment Shader 2

Vertexand and Fragmend Shader часть 2

Всем доброго времени суток! Надеюсь вы успешно переварили предыдущие
статьи по шейдерам ?)
Во второй части мы сделаем несколько более-сложный шейдер. Будем "добивать" тему с освещением: добавим блики иосвещенность, зависящую от нескольких источников света и сделаем втором сделаем эффект обводки(outline).
Прежде чем начнем писать сам шейдер, я хотел бы сделать небольшой ликбез
в способы ренера света, имеющиеся в юнити, а точнее по одному из них - упреждающему типу рендера (Forward Rendering).

Forward Rendering Path

Если мы сейчас создадим какой-нибудь дополнительный источник света, например point light, и поднесем его к нашей сфере, то мы не увидим никакого эффекта. Никакая из частей сферы не станет светлее,а всё потому, что мы описали в нашем шейдере освещение, зависящее только от одного источника света - Directional Light'а.Если мы хотим, чтобы объект мог взаимодействовать с несколькими источниками света, нужно как-то указать шейдеру эти источники и способы их ренера.Благо в движке этот момент предусмотрен. Юнити предлагает нам на выбор несколько способов рендера света.
Способ рендера(Render Path) - это набор инструкций для движка,определяющих,каким образом обрабатывать рендер света и теней. Unity поддерживает четыре типа рендера: Deferred,Forward,Legacy Deferred,Vertex Lit. Каждый из них имеет свои преимущества и недостатки.
Сравнение и краткое описание каждого из них вы можете найти здесь. Итак, упреждающий рендер.

При упреждающем рендеринге, некоторое количество самых ярких источников света, влияющих на каждый объект, рендерится в режиме полной попиксельной подсветки. Далее до 4 точечных источников света рассчитываются повертексно. Остальные источники света рассчитываются по сферическим гармоникам (Spherical Harmonics, SH), которые значительно быстрее, но выдают усреднённый результат.

  • Источники света всегда считаются повертексными или SH, если значение их свойства Render Mode (режим рендера) установлено в Not Important (не важно).
  • Самый яркий из направленных источников света всегда является попиксельным.
  • Источники света всегда считаются попиксельными, если значение их свойства Render Mode установлено в Important (важно).
  • Если в результате вышеперечисленного, количество источников света будет меньше текущего Pixel Light Count (количества пиксельных источников освещения), указанного в настройках качества, тогда в порядке убывания яркости, попиксельно отрендерится ещё некоторое количество источников света.

Для более полной картины, давайте рассмотрим пример авто-сортировки источников света, приводимый в документации юнити:

#4 Vertex and Fragment Shader 2 — Unity — DevTribe: Разработка игр

Если предположить, что в данном примере на объект действуют восемь одинаковых ( с одинаковым цветом и интенсивностью) источников света, а так-же на источниках выставлен Auto render-mode, то тогда они будут
сортироваться для данного объекта именно так:

#4 Vertex and Fragment Shader 2 — Unity — DevTribe: Разработка игр

Наиболее интенсивные источники света(A,B,C,D) будут рендерится в режиме per-pixel. Наименее яркие источники (D-G) будут рендерится в режиме per-vertex, а остальные (G,H) в SH режиме.Учтите, что группы источников света перекрывают друг друга. Например, последний попиксельный источник света переходит в повертексный режим освещения, поэтому будет меньше резких изменений освещения при перемещении объектов и источников света.

Если включен Directional Light, он считается движком самым ярким источником света

Вот как происходит рендеринг каждого объекта в упреждающем ренеринге

  • Базовый проход применяет один направленный попиксельный источник света( обычно Directional Light) и все повертексные/SH источники света.
  • Остальные попиксельные источники света рендерятся в дополнительных проходах, один проход на один

источник света.

Ну, надеюсь, теперь стало понятно. Мы с вами не станем делать полное Forward освещение, а выполним расчет только попиксельных источников. Т.е. Directionl Light + добавочные, наиболее яркие,источники света.
Что бы вручную указать, в каком режиме рендерить источник, нужно в компоненте Light, который есть у любого источника света, найти параметр Render Mode и выбрать нужное для нас значение:
Important - свет всегда просчитывается в per-pixel режиме.
Not Important - свет всегда просчитывается в per-vertex режиме.
Auto - использовался в примере, автоматически сортирует источники в зависимости от их яркости и близости к объекту.

#4 Vertex and Fragment Shader 2 — Unity — DevTribe: Разработка игр

Обычно упреждающий рендеринг стоит в юнити по умолчанию, но, на всякий случай, следует еще раз указать движку тип рендера. Для этого идем в настройки рендера (Edit > Project Settings>Player) и ставим значение Forward.

#4 Vertex and Fragment Shader 2 — Unity — DevTribe: Разработка игр

Phong Lighting

Одной из самых распространенных моделей освещения, поддерживающая блики,является модель освещения Фонга. Она высчитывает освещение по формуле:

#4 Vertex and Fragment Shader 2 — Unity — DevTribe: Разработка игр

или, как она будет записана у нас :
I = pow(max(0,dot(refl,view)),_Shininess)
refl= reflect(l,view)
l - вектор света
refl - направление отраженного света
Shininess - коэффициент блеска
view - направление взгляда

#4 Vertex and Fragment Shader 2 — Unity — DevTribe: Разработка игр

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

  Properties 
  {
    _xgmTex("Texture",2D) = "white" {} 
    _Color ("LogoColor",Color) = (0,0,0,0)
    _SpecColor("Specular Color",Color) = (0,0,0,0) // цвет блика
    _Shininess ("Shinines",Float) = 10 // коэффициент блеска материала
  }

Итак, сейчас мы делаем базовый проход, где будем рассчитывать самый яркий источник света ( в моем случае Directioal Light). Поставим нужный тег в Pass'е, что бы указать движку, что данный проход является базовым.

Pass
{ 
 Tags{"LightMode" = "ForwardBase"} // базовый проход 
...
}

Далее, объявим наши новые внешние переменные в CG коде:

     uniform sampler2D _xgmTex;
     uniform float4 _Color;
     uniform float4 _SpecColor;
     uniform float _Shininess;

Теперь, если внимательно посмотреть на формулу, то можно заметить, что нам не хватает вектора отраженного света и вектора взгляда. Вычислить отраженный вектор мы сможем с помощью функции reflect(float3 vec, float3 norm), где vec - вектор освещения, norm - вектор нормали.Оба этих вектора у нас уже есть. Вектор взгляда мы сможем вычислить, вычитая из мировых координат камеры, которые содержатся во встроенной переменной _WorldSpaceCameraPos, мировые координаты текущей точки. Надеюсь вы помните, что получить мировые координаты точки можно путем перемножения объектных координат на мировую матрицу( в юнити является так-же встроенной переменной - _Object2World).
Так-же, в этом шейдере мы не будем инвертировать цвета((1-col)), что бы нагляднее видеть блики.
Итак, для начала добавим в структуру vertexOutput переменную для мировых координат точки:

      struct vertexOutput
      {
       ...
       float3 worldPos:TEXCOORD2; 
      };

Далее перебираемся в вертексный шейдер и перемножаем объектные координаты на мировую матрицу:

      vertexOutput vert(vertexInput v) 
      {
        ...
        o.worldPos=mul(_Object2World,v.vertex); // переводим координаты из пространства модели в мировое 
        return o;
      }

Ну а теперь самое интересное - расчеты освещения в пиксельном шейдере :

 fixed4 frag(vertexOutput v):SV_Target
      {
        fixed4 col = tex2D(_xgmTex,v.uv); // берем цвет из текстуры по UV координатам
        float3 n = normalize(v.worldNormal); // нормализуем вектор нормали 
        float3 viewDir = normalize(_WorldSpaceCameraPos-v.worldPos); // вектор взгляда
        float3 l; // направление света 
        float atten; // коэффициент затенения
        if(_WorldSpaceLightPos0.w == 0)  // Directional light
        {
         l = normalize(_WorldSpaceLightPos0.xyz);
         atten = 1.0; // нет затенения
        }else{                                                  // point/spot light
         l = _WorldSpaceLightPos0.xyz - v.worldPos.xyz;
         atten = 1/length(l);
         l = normalize(l);
        }
        float3 dif = atten*col* max(0.0,dot(n,l)); // рассчитываем цвет освещенного пикселя
        float3 spec = float3(0,0,0);
        if(dot(l,n)>0.0)
        {
         float3 refl = reflect(-l,n);
         spec = atten * _SpecColor * pow(max(0.0,dot(refl,viewDir)),_Shininess);
        }
       
        return fixed4(dif+spec,1.0); 
      } 

Итак, мы сделали первый проход. Теперь Directioal Light создает блик на нашей сфере. Задав красный цвет для спекуляра и покрутив Directional Light я получил такой-вот блик.

#4 Vertex and Fragment Shader 2 — Unity — DevTribe: Разработка игр
Shader "MyShaders/xgmSkin" 
{
  Properties 
  {
    _xgmTex("Texture",2D) = "white" {} 
    _Color ("LogoColor",Color) = (0,0,0,0)
    _SpecColor("Specular Color",Color) = (0,0,0,0) // цвет блика
    _Shininess ("Shinines",Float) = 10 // коэффициент блеска материала
  }

SubShader
 {
   Pass
   {
     Tags{"LightMode"="ForwardBase"} // базовый проход
     Cull Off 

     CGPROGRAM 
     #pragma vertex vert // говорим имя у вертексного шейдера 
     #pragma fragment frag  // говорим имя пиксельного шейдера
     uniform sampler2D _xgmTex;
     uniform float4 _Color;
     uniform float4 _SpecColor;
     uniform float _Shininess;
      struct vertexInput 
      {
        float4 vertex:POSITION;
        float2 uv:TEXCOORD0;
        float3 norm:NORMAL;
      };
      struct vertexOutput
      {
       float4 position:SV_POSITION;
       float2 uv:TEXCOORD0;
       float3 worldNormal:TEXCOORD1;
       float3 worldPos:TEXCOORD2;
      };
      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);
        o.worldPos = mul(_Object2World,v.vertex); // переводим координаты из пространства модели в мировое 
        return o;
      }
 fixed4 frag(vertexOutput v):SV_Target
      {
        fixed4 col = tex2D(_xgmTex,v.uv); // берем цвет из текстуры по UV координатам
        float3 n = normalize(v.worldNormal); // нормализуем вектор нормали 
        float3 viewDir = normalize(_WorldSpaceCameraPos-v.worldPos); // вектор взгляда
        float3 l; // направление света 
        float atten; // коэффициент затенения
        if(_WorldSpaceLightPos0.w == 0)  // Directional light
        {
         l = normalize(_WorldSpaceLightPos0.xyz);
         atten = 1.0; // нет затенения
        }else{                                                  // point/spot light
         l = _WorldSpaceLightPos0.xyz - v.worldPos.xyz;
         atten = 1/length(l);
         l = normalize(l);
        }
        float3 dif = atten*col* max(0.0,dot(n,l)); // рассчитываем цвет освещенного пикселя
        float3 spec = float3(0,0,0);
        if(dot(l,n)>0.0)
        {
         float3 refl = reflect(-l,n);
         spec = atten * _SpecColor * col * pow(max(0.0,dot(refl,viewDir)),_Shininess);
        }
       
        return fixed4(dif+spec,1.0); 
      } 
     ENDCG  

   }

  } 
  Fallback "Diffuse"
 } 

Теперь напишем второй проход, где будем обрабатывать добавочные источники.
Создаем новый Pass после предыдущего,ставим ему тег добавочного прохода и добавляем смешивание(Blend), что бы этот Pass не перекрывал предыдущий:

    Pass
   {
    Tags{"LightMode" = "ForwardAdd"} // добавочный проход
    Blend One One 
   } 
Blend One One = складываем цвет текущего пикселя с цветом предыдущего

Т.к. предыдущий Pass у нас получился универсальным, весь CG код из него можно скопировать в этот и всё будет работать, т.к. мы сделали проверку переменной _WorldSpaceLightPos0 и в зависимости от того, какие данные она содержит,посчитали освещение.
Ну, в принципе вот и всё. У нас готов шейдер, который взаимодействует с несколькими источниками света и бликует.
Итог:

#4 Vertex and Fragment Shader 2 — Unity — DevTribe: Разработка игр

Shader "MyShaders/xgmSkin"
{
Properties
{
_xgmTex("Texture",2D) = "white" {}
_Color ("LogoColor",Color) = (0,0,0,0)
_SpecColor("Specular Color",Color) = (0,0,0,0) // цвет блика
_Shininess ("Shinines",Float) = 10 // коэффициент блеска материала
}

SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"} // базовый проход
Cull Off

CGPROGRAM
#pragma vertex vert // говорим имя у вертексного шейдера
#pragma fragment frag // говорим имя пиксельного шейдера
uniform sampler2D _xgmTex;
uniform float4 _Color;
uniform float4 _SpecColor;
uniform float _Shininess;
struct vertexInput
{
float4 vertex:POSITION;
float2 uv:TEXCOORD0;
float3 norm:NORMAL;
};
struct vertexOutput
{
float4 position:SV_POSITION;
float2 uv:TEXCOORD0;
float3 worldNormal:TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
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);
o.worldPos = mul(_Object2World,v.vertex); // переводим координаты из пространства модели в мировое
return o;
}
fixed4 frag(vertexOutput v):SV_Target
{
fixed4 col = tex2D(_xgmTex,v.uv); // берем цвет из текстуры по UV координатам
float3 n = normalize(v.worldNormal); // нормализуем вектор нормали
float3 viewDir = normalize(_WorldSpaceCameraPos-v.worldPos); // вектор взгляда
float3 l; // направление света
float atten; // коэффициент затенения
if(_WorldSpaceLightPos0.w == 0) // Directional light
{
l = normalize(_WorldSpaceLightPos0.xyz);
atten = 1.0; // нет затенения
}else{ // point/spot light
l = _ WorldSpaceLightPos0.xyz - v.worldPos.xyz;
atten = 1/length(l);
l = normalize(l);
}
float3 dif = atten*col* max(0.0,dot(n,l)); // рассчитываем цвет освещенного пикселя
float3 spec = float3(0,0,0);
if(dot(l,n)>0.0)
{
float3 refl = reflect(-l,n);
spec = atten * _SpecColor * col * pow(max(0.0,dot(refl,viewDir)),_Shininess);
}

return fixed4(dif+spec,1.0);
}
ENDCG

}

Pass
{
Tags{"LightMode" = "ForwardAdd"} // дополнительный проход
Blend One One
CGPROGRAM
#pragma vertex vert // говорим имя у вертексного шейдера
#pragma fragment frag // говорим имя пиксельного шейдера
uniform sampler2D _xgmTex;
uniform float4 _Color;
uniform float4 _SpecColor;
uniform float _Shininess;
struct vertexInput
{
float4 vertex:POSITION;
float2 uv:TEXCOORD0;
float3 norm:NORMAL;
};
struct vertexOutput
{
float4 position:SV_POSITION;
float2 uv:TEXCOORD0;
float3 worldNormal:TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
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);
o.worldPos = mul(_Object2World,v.vertex); // переводим координаты из пространства модели в мировое
return o;
}
fixed4 frag(vertexOutput v):SV_Target
{
fixed4 col = tex2D(_xgmTex,v.uv); // берем цвет из текстуры по UV координатам
float3 n = normalize(v.worldNormal); // нормализуем вектор нормали
float3 viewDir = normalize(_WorldSpaceCameraPos-v.worldPos); // вектор взгляда
float3 l; // направление света
float atten; // коэффициент затенения
if(_WorldSpaceLightPos0.w == 0) // Directional light
{
l = normalize(_WorldSpaceLightPos0.xyz);
atten = 1.0; // нет затенения
}else{ // point/spot light
l = _ WorldSpaceLightPos0.xyz - v.worldPos.xyz;
atten = 1/length(l);
l = normalize(l);
}
float3 dif = atten*col* max(0.0,dot(n,l)); // рассчитываем цвет освещенного пикселя
float3 spec = float3(0,0,0);
if(dot(l,n)>0.0)
{
float3 refl = reflect(-l,n);
spec = atten * _SpecColor * col * pow(max(0.0,dot(refl,viewDir)),_Shininess);
}

return fixed4(dif+spec,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}

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

OutLine Shader

Ну, чего тянуть? Давайте сразу взглянем на результат )

#4 Vertex and Fragment Shader 2 — Unity — DevTribe: Разработка игр

Вот это и есть outline шейдер. Он создаем эффект того, что объект обведен линией.Ладно,хватит слов,приступим к написанию!
Добавим новые внешние переменные в блок Properties

  Properties 
  {
    _xgmTex("Texture",2D) = "white" {} 
    _Color ("LogoColor",Color) = (0,0,0,0)
    _SpecColor("Specular Color",Color) = (0,0,0,0) // цвет блика
    _Shininess ("Shinines",Float) = 10 // коэффициент блеска материала
    _OutColor("Outline Color",Color) = (0,0,0,0) // цвет обводки
    _Outline("Outline",Range(0.01,0.05)) = 0.05 // размер 
  }

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

Pass 
{
 Cull Front  // отсекаем полигоны спереди
 ZTest Always // делаем Wall Hack "эффект " 
}

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

 CGPROGRAM
     #pragma vertex vert 
     #pragma fragment frag 
     #include "UnityCG.cginc"
     uniform float _Outline;
     uniform float4 _OutColor;

Для того, что-бы "раздуть" объект, многого не требуется. Нам будет нужна только нормаль, ну и координаты вершины. Напишем входную и выходную структуры для вертексного шейдера:

 struct vertexInput 
     {
      float4 vertex:POSITION;
      float3 normal:NORMAL;
     };       
     struct vertexOutput 
     {
      float4 pos:SV_POSITION;
      float4 col:COLOR;
     };

А теперь пришла очередь главному герою появится в коде, итак, дамы и господа, вертексный шейдер!

     vertexOutput vert (vertexInput v)
     {
      vertexOutput o;
      o.pos = mul(UNITY_MATRIX_MVP,v.vertex); 
      float3 viewNorm = mul((float3x3)UNITY_MATRIX_IT_MV,v.normal); // получаем нормаль в видовых координатах
      float2 NormXY= TransformViewToProjection(viewNorm.xy); // функция из CGinclude. Переводим xy координаты нормали в проекционное пространство 
      o.pos.xy += NormXY * _Outline; // "раздуваем модел"
      o.col = _OutColor; 
      return o;
     }

Уже почти готово, осталось только задать цвет в пиксельном шейдере:

     fixed4 frag (vertexOutput v):SV_Target
     {
      return v.col;
     }

Всё! Шейдер готов.

Shader "MyShaders/xgmSkin" 
{
  Properties 
  {
    _xgmTex("Texture",2D) = "white" {} 
    _Color ("LogoColor",Color) = (0,0,0,0)
    _SpecColor("Specular Color",Color) = (0,0,0,0) // цвет блика
    _Shininess ("Shinines",Float) = 10 // коэффициент блеска материала
    _OutColor("Outline Color",Color) = (0,0,0,0) // цвет обводки
    _Outline("Outline",Range(0.01,0.05)) = 0.05 // размер обводки 
  }

SubShader
 {
  Tags{"RenderType" = "Transparent"}
   Pass
   {
     Cull Front 
     ZTest Always
     Blend SrcAlpha OneMinusSrcAlpha 
     CGPROGRAM
     #pragma vertex vert 
     #pragma fragment frag 
     #include "UnityCG.cginc"
     uniform float _Outline;
     uniform float4 _OutColor;
     struct vertexInput 
     {
      float4 vertex:POSITION;
      float3 normal:NORMAL;
     };       
     struct vertexOutput 
     {
      float4 pos:SV_POSITION;
      float4 col:COLOR;
     };

     vertexOutput vert (vertexInput v)
     {
      vertexOutput o;
      o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
      float3 viewNorm = mul((float3x3)UNITY_MATRIX_IT_MV,v.normal); // получаем нормаль в видовых координатах 
      float2 viewNormXY= TransformViewToProjection(viewNorm.xy); // функция из CGinclude.Переводим координаты нормали в проекционное простанство 
      o.pos.xy += viewNormXY * _Outline; // "раздуваем модель" 
      o.col = _OutColor;
      return o;
     }
     fixed4 frag (vertexOutput v):SV_Target
     {
      return v.col;
     }
     ENDCG
   }

   Pass
   {
     Tags{"LightMode"="ForwardBase"} // базовый проход
     Cull Off 

     CGPROGRAM 
     #pragma vertex vert // говорим имя у вертексного шейдера 
     #pragma fragment frag  // говорим имя пиксельного шейдера
     uniform sampler2D _xgmTex;
     uniform float4 _Color;
     uniform float4 _SpecColor;
     uniform float _Shininess;
      struct vertexInput 
      {
        float4 vertex:POSITION;
        float2 uv:TEXCOORD0;
        float3 norm:NORMAL;
      };
      struct vertexOutput
      {
       float4 position:SV_POSITION;
       float2 uv:TEXCOORD0;
       float3 worldNormal:TEXCOORD1;
       float3 worldPos:TEXCOORD2;
      };
      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);
        o.worldPos = mul(_Object2World,v.vertex); // переводим координаты из пространства модели в мировое 
        return o;
      }
 fixed4 frag(vertexOutput v):SV_Target
      {
        fixed4 col = tex2D(_xgmTex,v.uv); // берем цвет из текстуры по UV координатам
        float3 n = normalize(v.worldNormal); // нормализуем вектор нормали 
        float3 viewDir = normalize(_WorldSpaceCameraPos-v.worldPos); // вектор взгляда
        float3 l; // направление света 
        float atten; // коэффициент затенения
        if(_WorldSpaceLightPos0.w == 0)  // Directional light
        {
         l = normalize(_WorldSpaceLightPos0.xyz);
         atten = 1.0; // нет затенения
        }else{                                                  // point/spot light
         l = _WorldSpaceLightPos0.xyz - v.worldPos.xyz;
         atten = 1/length(l);
         l = normalize(l);
        }
        float3 dif = atten*col* max(0.0,dot(n,l)); // рассчитываем цвет освещенного пикселя
        float3 spec = float3(0,0,0);
        if(dot(l,n)>0.0)
        {
         float3 refl = reflect(-l,n);
         spec = atten * _SpecColor* pow(max(0.0,dot(refl,viewDir)),_Shininess);
        }
       
        return fixed4(dif+spec,1.0); 
      } 
     ENDCG  

   }

   Pass
   {
    Tags{"LightMode" = "ForwardAdd"} // дополнительный проход
    Blend One One 
    CGPROGRAM 
     #pragma vertex vert // говорим имя у вертексного шейдера 
     #pragma fragment frag  // говорим имя пиксельного шейдера
     uniform sampler2D _xgmTex;
     uniform float4 _Color;
     uniform float4 _SpecColor;
     uniform float _Shininess;
      struct vertexInput 
      {
        float4 vertex:POSITION;
        float2 uv:TEXCOORD0;
        float3 norm:NORMAL;
      };
      struct vertexOutput
      {
       float4 position:SV_POSITION;
       float2 uv:TEXCOORD0;
       float3 worldNormal:TEXCOORD1;
       float3 worldPos:TEXCOORD2;
      };
      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);
        o.worldPos = mul(_Object2World,v.vertex); // переводим координаты из пространства модели в мировое 
        return o;
      }
 fixed4 frag(vertexOutput v):SV_Target
      {
        fixed4 col = tex2D(_xgmTex,v.uv); // берем цвет из текстуры по UV координатам
        float3 n = normalize(v.worldNormal); // нормализуем вектор нормали 
        float3 viewDir = normalize(_WorldSpaceCameraPos-v.worldPos); // вектор взгляда
        float3 l; // направление света 
        float atten; // коэффициент затенения
        if(_WorldSpaceLightPos0.w == 0)  // Directional light
        {
         l = normalize(_WorldSpaceLightPos0.xyz);
         atten = 1.0; // нет затенения
        }else{                                                  // point/spot light
         l = _WorldSpaceLightPos0.xyz - v.worldPos.xyz;
         atten = 1/length(l);
         l = normalize(l);
        }
        float3 dif = atten*col* max(0.0,dot(n,l)); // рассчитываем цвет освещенного пикселя
        float3 spec = float3(0,0,0);
        if(dot(l,n)>0.0)
        {
         float3 refl = reflect(-l,n);
         spec = atten * _SpecColor * pow(max(0.0,dot(refl,viewDir)),_Shininess);
        }
       
        return fixed4(dif+spec,1.0); 
      } 
     ENDCG  
   }
  } 
  Fallback "Diffuse"
 } 

Как всё выглядит в игре:
http://devtribe.ru/ext-files/562/177368/qwerty.unity3d


to be continued...


статья уже пригодилась

Справка