Mariya Mariya

Dreaman, Спасибо! =)

Dreaman Dreaman

Mariya, законченный вариант реально классно выглядит! Молодец :)

Mariya Mariya

Всем привет!
Сегодня хочу показать законченный вариант домика Сырны.

alexprey alexprey

StarPlosion: Битва с пиратами за планету

Wings' might Wings' might

Всем привет)
Добавил на этой неделе начальное окно, новую валюту и достижения:

alexprey alexprey

shadeborn, может быть они нашли способ как обойти полноценный запуск этих сервисов при запуске игры? Если так, то почему бы и не использовать единый клиент?) В любом случае, чем дальше все идет, там это все больше становится похоже на эту шутку

...
shadeborn shadeborn

Это всё круто, но...зачем? Ок есть Стим. Хорошо, теперь еще и ЕГС есть. Благо, комп не зашкварен Оригином...но у людей и он стоит. И для работы игр внутри этих сервисов нужны, собсно, эти сервисы. Так поверх этих сервисов нужно накатить еще один...

Devion Devion

согласен, есть кейсы которые без них вообще не делаются, не знать или избегать их определенно неправильно

Так точно, либо любое изменение в объекте на стороне редактора.

alexprey alexprey

Devion, только рад за дополнение, Спасибо)

хм, а вот это интересно, не пробовал никогда такой кейс.

Devion Devion

лёш, ты же не против если я дополню? )

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

Mariya Mariya

Всем привет!
Начали работу над мебелью в домик Сырны, и первым сделали чайный столик с чайным сервизом. А так же продолжаем работу над анимациями.

Wings' might Wings' might

Всем привет)
За неделю в игру было добавлено меню настроек, переделана старая локация, добавлены новые враги и повышена производительности

alexprey alexprey

А мне кажется это такой особый хитрый ход, хотя с другой стороны как разработчики успеют к этому подготовиться получше 🤔

Dreaman Dreaman

Всем привет!
Для проекта "Mental State" разработано новое устройство, которое уже полностью функционирует внутри игрового мира. Оно носит название "Репульсивер". Совместно с очередными большими воротами это устройство образует новую головоломку...

...
Mariya Mariya

Всем привет!
На этой неделе мы научили Сырну летать!

Tartal Tartal

alexprey, кастомизации - создание внешности персонажа? Я всегда любил это, но не думаю, что это будет к месту в мясном шутере)
EfimovMax, да, есть немного)

Tartal Tartal

Jusper, точно не помню, уже как полгода точно) Я вроде в Дискорде немного обсуждал эту тему. Пока немножко попробовал движок - мне очень нравится. Ну, в конце концов, он идеально подходит под жанры, с которыми я хочу работать...

alexprey alexprey

Первая тема крутая очень!

Логотип проекта 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...


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