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 (количества пиксельных источников освещения), указанного в настройках качества, тогда в порядке убывания яркости, попиксельно отрендерится ещё некоторое количество источников света.
Для более полной картины, давайте рассмотрим пример авто-сортировки источников света, приводимый в документации юнити:
Если предположить, что в данном примере на объект действуют восемь одинаковых ( с одинаковым цветом и интенсивностью) источников света, а так-же на источниках выставлен Auto render-mode, то тогда они будут
сортироваться для данного объекта именно так:
Наиболее интенсивные источники света(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 - использовался в примере, автоматически сортирует источники в зависимости от их яркости и близости к объекту.
Обычно упреждающий рендеринг стоит в юнити по умолчанию, но, на всякий случай, следует еще раз указать движку тип рендера. Для этого идем в настройки рендера (Edit > Project Settings>Player) и ставим значение Forward.
Phong Lighting
Одной из самых распространенных моделей освещения, поддерживающая блики,является модель освещения Фонга. Она высчитывает освещение по формуле:
или, как она будет записана у нас :
I = pow(max(0,dot(refl,view)),_Shininess)
refl= reflect(l,view)
l - вектор света
refl - направление отраженного света
Shininess - коэффициент блеска
view - направление взгляда
Ну что-же, с теорией вроде-бы разобрались,самое время переходить к практике! Берем наш старый диффузный шейдер и начинаем творить.
Для начала нужно добавить недостающие параметры в блок 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 я получил такой-вот блик.
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 }
Т.к. предыдущий Pass у нас получился универсальным, весь CG код из него можно скопировать в этот и всё будет работать, т.к. мы сделали проверку переменной _WorldSpaceLightPos0 и в зависимости от того, какие данные она содержит,посчитали освещение.
Ну, в принципе вот и всё. У нас готов шейдер, который взаимодействует с несколькими источниками света и бликует.
Итог:
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
Ну, чего тянуть? Давайте сразу взглянем на результат )
Вот это и есть 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...
CollectableItemData.cs
[CreateMenuItem(fileName = "newItem", menuName = "Data/Items/Collectable", order = 51]