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

#5 Vertex and Fragment Shader 3

Vertexand and Fragmend Shader часть 3

!Всем! доброго времени суток! Ну, вот мы и добрались до финальной в серии статьи по вертексному и фрагментному шейдеру. Следующие статьи будут посвящены уже поверхностному (surface) шейдеру. А в данной статье мы напишем шейдер с картой нормали. Для понимания, что тут вообще происходит, нужно ознакомиться с предыдущими статьями. Нус, начнем !

Введение

!Думаю! и без слов понятно, что графика в игре является очень важным элементом, что игроки очень пристальное внимание уделяют именно графике. Поэтому, чем мы, разработчики шедевров, лучшую графику завезем, тем лучше будет впечатление от нашей игры. Мы с вами уже изучили базовые элементы для создания шейдеров, но хорошей картинки из этих шейдеров не выжать. Объекты с нашими шейдерами не выглядят реалистично. Глаз требует наличия большей детализированности объектов( всякие шероховатости, трещины, сколы, неровности,складки и т.д.) Это можно, конечно, сделать с помощью геометрии, но будет очень затратно по ресурсам, так как для этого потребуется неимоверное количество полигонов. Есть способ сделать более быстрым способом - рельефным текстурированием.

Краткая теория

Рельефное текстурирование - метод в компьютерной графике для придания более реалистичного и насыщенного вида поверхности объектов. При рельефном текстурировании мы не изменяем геометрию модели, т.е. не делаем "честной" детализации, а работаем с освещением или текстурными координатами, изменяя их в соответствии с рельефными текстурами.

Рельефная текстура - текстура (чаще карта), которая, в отличии от простых текстур, несущих информацию о цвете участков объекта,несет информацию о неровностях объекта.

Тексель (сокращение от англ. Texture element) — минимальная единица текстуры трёхмерного объекта. Пиксель текстуры.
На данный момент существует достаточно большое количество различных методов рельефного текстурирования, причем каждый отдельный способ имеет свою определенную рельефную текстуру.

Основные способы рельефного текстурирования

Bump mapping basic

Bump mapping basic — простой способ создания эффекта рельефной поверхности с детализацией бОльшей, чем позволяет полигональная поверхность. Эффект главным образом достигается за счет расчета освещения при помощи карты высот. "Высота" в такое карте закодирована в каждом текселе: чем светлее тексель, тем "выше" предпологаемая высота объекта.Как правило Bump mapping позволяет создать не очень сложные бугристые поверхности, плоские выступы или впадины, на этом его использование заканчивается. Для более детальных эффектов впоследствии был придуман Normal mapping.

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

Normal mapping

Вот тут мы остановимся чуть поподробнее.

Normal mapping - техника задания вектора , позволяющая изменять нормаль отображаемого пикселя основываясь на цветной карте нормалей, в которой координаты нормали хранятся в виде текселя, цветовые составляющие которого [r,g,b] интерпретируются в оси вектора [x, y, z], на основе которого вычисляется нормаль, используемая для расчета освещенности пикселя. Благодаря тому, что в карте нормалей задействуются 3 канала текстуры, этот метод дает большую точность, чем Bump mapping, в котором используется только один канал и нормали, по сути, всего лишь интерпретируются в зависимости от «высоты». Каждый компонент карты нормалей (r, g или b) варьируется в промежутке от 0 до 1.

!Существуют! два способа кодирования векторов в карты нормалей :

  • object space - Координаты вектора нормали задаются в мировых (общих) координатах. Используется в основном для статических объектов( стены, окна, двери и т.д.)
  • tangent space - Координаты вектора нормали задаются в "касательном" пространстве, т.е. в локальной системе координат треуголника. Наиболее используемый в играх метод. Мы с вами также будет работать с этим ним.

!Типичный! представитель tangent-space карт :

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

!Итак!, мы уже уяснили, что в цвет закодированы координаты нормали. Давайте теперь разбираться как.
Во-первых, нам нужно понимать, что вектор нормали может быть не только положительным, но и отрицательным. Поэтому, просто так закодировать в цвет не получится (т.к. цвет в пределах от 0 до 1).
!Чтобы! всё работало как нужно, *x* и *y* компоненты вектора нормали (Nx,Ny,Nz) нужно закодировать в цвет (R,G,B,A) по формуле :
A = (Nx+1)/2
G= (Ny+1)/2
Следовательно, когда декодируем нормаль:
Nx = 2*A - 1
Ny = 2*G - 1
!Почему! кодируем только 2 компонента ? Для ответа на этот вопрос следует еще раз вспомнить, что такое нормаль. Нормаль - это направление. Нам не важна её длина, поэтому сам вектор кодируют в нормализованном виде. А это значит, что длина вектора нормали равна единице. Если немного поколдовать, можно получить формулу для третьего компонента:

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

!Благо! все эти преобразования в шейдере мы можем не делать. Для этих целей можно воспользоваться встроенной фунецией ?UnpackNormal?. С этим разобрались, двигаемся дальше!

!Теперь! сосредоточим наше внимание вот на чём: вектор нормали кодируется в tangent-space, также,движок юнити поставляет касательную (tangent) к поверхности и нормаль, которые закодированы тоже в tangent-space, а расчеты нам нужно производить в world-space. Вывод напрашивается сам собой. Нужно перевести нормаль (направление) в мировую систему координат.

!Локальная! система координат треугольника выглядит вот так:

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

!Чтобы! осуществить переход от локальной системы координат к мировой, нужно составить матрицу перехода.Глубоко в этот процесс уходить не будем, лишь запомним, что для того, чтобы правила умножения матриц на вектора работали, матрица перехода из локального в мировое пространство должна иметь вид :

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

Где *T* - касательная (tangent) // ось X
*B* - вторая кастельная (bitangent) // ось Y
*N* - нормаль поверхности треугольника (normal) // ось Z

!Алгоритм! составления матрицы перехода и получения нормали из текстуры в мировых координатах:

  1. Переводим нормаль(не закодированную в текстуре, а нормаль полигона) и касательную(tangent) в мировые координаты.
  2. С помощью векторного умножения находим недостающий Bitangent вектор, который сразу будет в мировых координатах.
  3. Из этих векторов составляем матрицу перехода в виде, как показано на рисунке выше.
  4. Декодируем нормаль из текстуры.
  5. Скалярно перемножаем каждый компонент декодированный нормали с соответствующим столбцом в матрице.

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


Parallax mapping

Parallax mapping - метод рельефного текстурирования, основанная на смещении ?UV? координат в зависимости от взгляда. Такой метод хорош для поверхностей с плавно изменяющимися высотами, без просчета пересечений и больших значений смещения.

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

!Видите! камни, которые выступают из стены ? Так вот, это обычная текстура камня, но её текстурные координаты изменены с помощью parallax mapping'а.

Плюсы и минусы методов рельефного текстурирования

Общим +плюсом+ всех методов является их способность создавать "рельеф" без миллионов вершин и полигонов, а общим !минусом! является наличие дополнительной текстуры(карты), которая требует памяти.
Bump Mapping
+ Относительно небольшой размер текстуры

- Небольшая точность передачи, можно описать только простые формы

Normal Mapping
+ Более точное описание "рельефа" поверхности
+ Лучше подходят для низкополигональных моделей

- Эффект работает, только если смотреть на текстуру прямо или под небольшим углом.
- Требует доп. вычисления, хотя и не очень "дорогих"

Parallax Mapping
+ Те же плюсы, что и у normal mappinga
+ Эффект работает под любым углом
+ Наилучший(из относительно "дешевых") способ добиться объемного отображения текстуры

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

Тайлинг и смещение текстуры

!Для! того, чтобы сделать тайлинг (tiling) или смещение(offset) текстуры, юнити автоматически в инспекторе к каждой текстуре добавляет такие вот вещи :

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

!В! них мы можем задать *x* и *y* тайлинга и смещения. Чтобы получить доступ к этим параметрам в шейдере, мы должны объявить внешнюю переменную типа ?float4?, имеющую такое-же имя, как и текстура, но в конце к которой приписываем "*_ST*". В эту переменную движок передаст заданные в инспекторе значения в таком виде
variable_ ST.xy - *x* и *y* параметры тайлинга
variable_ ST.zw - *x* и *y* параметры смещения.

	Shader "Example_Shader" 
	{
		 Properties 
		{
			_SomeTexture("Some_Texture",2D) = "white"{}
		}
		
		SubShader 
		{
			Pass
			{
				... 
				uniform sampler2D _SomeTexture;
				uniform float4 _SomeTexture_ST; 
				...
				
				float4 frag(appdata v) :SV_Target
				{
					// достаем цвет на основе uv координат с учетом тайлинга и смещения
					float4 color = tex2D(_SomeTexture,v.uv * _SomeTexture.xy + _SomeTexture.zw);
					...
				}
			}

		}
	}

Пишем шейдер

!Переходим! к практике.
Создадим каркас для нашего шейдера на основе кода предыдущего, где мы программировали освещение по Фонгу с дополнительным проходом, но в этом шейдере добавим дополнительный прорход, в котором мы накладываемым лого xgm'а ).

обратите внимание, здесь наши входная(vertexInput) и выходная(vertexOutput) структуры поменяли названия на те, // которые чаще используются на практике.
Shader "xgm/xgm_normal"
{
	Properties
	{
		_Texture("Main_Texture",2D) = "whute"{}
		_xgmTexture("xgm_Texture",2D) = "white"{}
		_Color("Color",Color) = (1,1,1,1)
		_SpecColor("Specular_Color",Color) = (1,1,1,1)
		_xgmColor("xgm_Color",Color) = (1,1,1,1)
		_Shininess("Shininess",Float) = 10
		_AO("Ambient_Occlusion",Float) = 0.5

	}

	SubShader
	{
		Pass
		{
			Tags{"LightMode" = "ForwardBase"}
			Cull off
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "UnityCG.cginc"

				uniform sampler2D _Texture;
				uniform float4 _Texture_ST;
				uniform float4 _Color;
				uniform float4 _SpecColor;
				uniform float _Shininess;
				uniform float _AO;

				struct appdata // Vertex Input structure
				{
					float4 vertex:POSITION;
					float3 normal:NORMAL;
					float2 uv:TEXCOORD0;
				};
				struct v2f  // Vertex output structure
				{
					float4 pos:SV_POSITION;
					float2 uv:TEXCOORD0;
					float4 worldPos:TEXCOORD1;
					float4 worldNormal:TEXCOORD2;
				};
				v2f vert(appdata v)
				{
					v2f o;
					o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
					o.uv = v.uv;
					o.worldNormal = mul(unity_WorldToObject, v.normal);
					o.worldPos = mul(unity_ObjectToWorld, v.vertex);
					return o;

				}

				float4 frag(v2f v) : SV_Target
				{ 
					float4 col = tex2D(_Texture,v.uv);
					float3 worldNormal = normalize(v.worldNormal);
					// Делаем освещение по фонгу 
					float3 viewDir = normalize(_WorldSpaceCameraPos - v.worldPos);
					float3 l; // направление света 
					float3 atten; // затенение 
					if (_WorldSpaceLightPos0.w == 0)
					{
						l = normalize(_WorldSpaceLightPos0.xyz);
						atten = 1.0;
					}else {
						l = _WorldSpaceLightPos0.xyz - v.worldPos;
						atten = 1 / length(l);
						l = normalize(l);
					}
					float3 AO = (UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb) * _AO;
					float3 diff = atten * col * max(0.0, dot(l, worldNormal)); 
					float3 spec = float3(0.0, 0.0, 0.0);
					if (dot(l, v.worldNormal) > 0.0)
					{
						float3 refl = reflect(-l, worldNormal);
						spec = atten * _SpecColor * col * pow(max(0.0, dot(refl, viewDir)), _Shininess);
					}
					
					return float4(diff + spec + AO, 1.0);

				}

			ENDCG
		}

		Pass
		{
			Tags{"LightMode" = "ForwardAdd"}
			Blend One One
			Cull off
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "UnityCG.cginc"

				uniform sampler2D _Texture;
				uniform float4 _Texture_ST;
				uniform float4 _Color;
				uniform float4 _SpecColor;
				uniform float _Shininess;

				struct appdata // Vertex Input structure
				{
					float4 vertex:POSITION;
					float3 normal:NORMAL;
					float2 uv:TEXCOORD0;
				};
				struct v2f  // Vertex output structure
				{
					float4 pos:SV_POSITION;
					float2 uv:TEXCOORD0;
					float4 worldPos:TEXCOORD1;
					float4 worldNormal:TEXCOORD2;
				};
				v2f vert(appdata v)
				{
					v2f o;
					o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
					o.uv = v.uv;
					o.worldNormal = mul(unity_WorldToObject, v.normal);
					o.worldPos = mul(unity_ObjectToWorld, v.vertex);
					return o;

				}

				float4 frag(v2f v) : SV_Target
				{ 
					float4 col = tex2D(_Texture,v.uv);
					float3 worldNormal = normalize(v.worldNormal);
					// Делаем освещение по фонгу 
					float3 viewDir = normalize(_WorldSpaceCameraPos - v.worldPos);
					float3 l; // направление света 
					float3 atten; // затенение 
					if (_WorldSpaceLightPos0.w == 0)
					{
						l = normalize(_WorldSpaceLightPos0.xyz);
						atten = 1.0;
					}else{
						l = _WorldSpaceLightPos0.xyz - v.worldPos;
						atten = 1 / length(l);
						l = normalize(l);
					}
					float3 diff = atten * col * max(0.0, dot(l, worldNormal)); 
					float3 spec = float3(0.0, 0.0, 0.0);
					if (dot(l, v.worldNormal) > 0.0)
					{
						float3 refl = reflect(-l, worldNormal);
						spec = atten * _SpecColor * col * pow(max(0.0, dot(refl, viewDir)), _Shininess);
					}
					
					return float4(diff + spec, 1.0);

				}

			ENDCG
		}
		Pass
		{
			Blend DstAlpha OneMinusSrcAlpha
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
		uniform sampler2D _xgmTexture;
		uniform float4 _xgmTexture_ST;
		uniform float4 _xgmColor;
		struct appdata // Vertex Input structure
		{
			float4 vertex:POSITION;
			float2 xgmUV:TEXCOORD0;
		};
		struct v2f  // Vertex output structure
		{
			float4 pos:SV_POSITION;
			float2 xgmUV:TEXCOORD0;
		};

		v2f vert(appdata v)
		{
			v2f o;
			o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
			o.xgmUV = v.xgmUV;
			return o;

		}
		float4 frag(v2f v) :SV_Target
		{
			float4 col = tex2D(_xgmTexture,v.xgmUV * _xgmTexture_ST.xy + _xgmTexture_ST.zw);
			return ((1 - col)*(_xgmColor.r*0.5*_xgmColor));
		}
			ENDCG
		}
	}
}

Основа есть, теперь пора добавлять нормали! Добавим новую переменную текстуры в блок Properties :

_Normal_map("Normal_map",2D) = "white"{}

И сразу-же в обоих проходах новые пропишем внешние переменные для текстуры и её параметров (тайлинга и смещения):

uniform sampler2D _Normal_map;
uniform float4 _Normal_map_ST;

!Теперь!, чтобы у нас была возможность перевести нормали в мировые координаты, необходимо запросить касательную (tangent), ну и добавить слот под ?UV? координаты карты нормалей:

				struct appdata // Vertex Input structure
				{
					float4 vertex:POSITION;
					float3 normal:NORMAL;
					float2 uv:TEXCOORD0;
					float2 normUV:TEXCOORD1;
					float4 tangent:TANGENT; // касательная 

				};

!Итак!. Теперь нужно остановиться и собраться с мыслями. Чтобы правильно перевести закодированную нормаль в мировые координаты, нам нужна матрица перехода. Считать её мы будет в вертексном шейдере и передавать в пиксельный. Для более удобного представления этой матрицы мы введем 3 переменных типа ?float3?, каждая из которых будет считаться *строкой* матрицы перехода. Вот так будет выглядеть наша выходня структура:

		struct v2f  // Vertex output structure
		{
			float4 pos:SV_POSITION;
			float2 uv:TEXCOORD0;
			float4 worldPos:TEXCOORD1;
			float4 worldNormal:TEXCOORD2;
			float2 normUV:TEXCOORD3;
			float3 tspace1 :TEXCOORD4;	// первая строка матрицы
			float3 tspace2 :TEXCOORD5;	// вторая строка матрицы
			float3 tspace3 : TEXCOORD6;	// третья строка матрицы
		};

!Все! структуры написаны, значит, следующим шагом будет написание вертексного шейдера. Напомню, в нём мы будем составлять матрицу перехода из касательного пространства в мировое.
Создадим новую переменную worldTangent, которая будет содержать вектор касательной в мировых координатах,а также переменную worldBitangent, являющуюся перпендикулярной плоскости, образуемой векторами нормали и касательной:

		float3 worldTangent = mul(unity_ObjectToWorld, v.tangent); // получаем касательную в мировых коор-х
		float3 worldBitangent = cross(o.worldNormal, worldTangent); // получаем bitangent путем векторного       перемножения 	 нормали и касательной

!Теперь! у нас есть все базисные вектора для построения матрицы перехода, строим:

			// Строим матрицу перехода 
			o.tspace1 = float3(worldTangent.x, worldBitangent.x, o.worldNormal.x);
			o.tspace2 = float3(worldTangent.y, worldBitangent.y, o.worldNormal.y);
			o.tspace3 = float3(worldTangent.z, worldBitangent.z, o.worldNormal.z);

Вертексный шейдер готов!

!Ну! и последний шитрих - написание пиксельного шейдера. В нём мы будем распаковывать нормаль и переводить в мировые координаты.
Добавим переменную, содержащую в себе декодированную из текстуры нормаль

float3 tangentNormal = UnpackNormal(tex2D(_Normal_map,v.normUV*_Normal_map_ST.xy+_Normal_map_ST.zw)); // получаем нормаль в локальном пространстве
float3 worldNormal;

И приступим к последовательному перемножению каждого компонента нормали на матрицу перехода по всем правилам матрично-векторного умножения ( строка на столбец):

		worldNormal.x = mul(tangentNormal, v.tspace1);
		worldNormal.y = mul(tangentNormal, v.tspace2);
		worldNormal.z = mul(tangentNormal, v.tspace3);

!Вот! теперь для расчетов у нас есть нормаль из карты. Используя её для расчета света я получил примерно такие вот результаты:

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

Для сравнения объект с картой нормалей и без:

#5 Vertex and Fragment Shader 3 — Unity — DevTribe: Разработка игр
Shader "xgm/xgm_normal"
{
	Properties
	{
		_Texture("Main_Texture",2D) = "whute"{}
		_Normal_map("Normal_map",2D) = "white"{}
		_xgmTexture("xgm_Texture",2D) = "white"{}
		_Color("Color",Color) = (1,1,1,1)
		_SpecColor("Specular_Color",Color) = (1,1,1,1)
		_xgmColor("xgm_Color",Color) = (1,1,1,1)
		_Shininess("Shininess",Float) = 10
		_AO("Ambient_Occlusion",Float) = 0.5

	}

	SubShader
	{
		Pass
		{
			Tags{"LightMode" = "ForwardBase"}
			Cull off
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "UnityCG.cginc"

				uniform sampler2D _Texture;
				uniform float4 _Texture_ST;
				uniform sampler2D _Normal_map;
				uniform float4 _Normal_map_ST;
				uniform float4 _Color;
				uniform float4 _SpecColor;
				uniform float _Shininess;
				uniform float _AO;

				struct appdata // Vertex Input structure
				{
					float4 vertex:POSITION;
					float3 normal:NORMAL;
					float2 uv:TEXCOORD0;
				};
				struct v2f  // Vertex output structure
				{
					float4 pos:SV_POSITION;
					float2 uv:TEXCOORD0;
					float4 worldPos:TEXCOORD1;
					float4 worldNormal:TEXCOORD2;
				};
				v2f vert(appdata v)
				{
					v2f o;
					o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
					o.uv = v.uv;
					o.worldNormal = mul(unity_WorldToObject, v.normal);
					o.worldPos = mul(unity_ObjectToWorld, v.vertex);
					return o;

				}

				float4 frag(v2f v) : SV_Target
				{ 
					float4 col = tex2D(_Texture,v.uv);
					float3 worldNormal = normalize(v.worldNormal);
					// Делаем освещение по фонгу 
					float3 viewDir = normalize(_WorldSpaceCameraPos - v.worldPos);
					float3 l; // направление света 
					float3 atten; // затенение 
					if (_WorldSpaceLightPos0.w == 0)
					{
						l = normalize(_WorldSpaceLightPos0.xyz);
						atten = 1.0;
					}else {
						l = _WorldSpaceLightPos0.xyz - v.worldPos;
						atten = 1 / length(l);
						l = normalize(l);
					}
					float3 AO = (UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb) * _AO;
					float3 diff = atten * col * max(0.0, dot(l, worldNormal)); 
					float3 spec = float3(0.0, 0.0, 0.0);
					if (dot(l, v.worldNormal) > 0.0)
					{
						float3 refl = reflect(-l, worldNormal);
						spec = atten * _SpecColor * col * pow(max(0.0, dot(refl, viewDir)), _Shininess);
					}
					
					return float4(diff + spec + AO, 1.0);

				}

			ENDCG
		}

		Pass
		{
			Tags{"LightMode" = "ForwardAdd"}
			Blend One One
			Cull off
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "UnityCG.cginc"

				uniform sampler2D _Texture;
				uniform float4 _Texture_ST;
				uniform sampler2D _Normal_map;
				uniform float4 _Normal_map_ST;
				uniform float4 _Color;
				uniform float4 _SpecColor;
				uniform float _Shininess;

				struct appdata // Vertex Input structure
				{
					float4 vertex:POSITION;
					float3 normal:NORMAL;
					float2 uv:TEXCOORD0;
					float2 normUV:TEXCOORD1;
					float4 tangent:TANGENT;

				};
				struct v2f  // Vertex output structure
				{
					float4 pos:SV_POSITION;
					float2 uv:TEXCOORD0;
					float4 worldPos:TEXCOORD1;
					float4 worldNormal:TEXCOORD2;
					float2 normUV:TEXCOORD3;
					float3 tspace1 :TEXCOORD4; // первая строка матрицы
					float3 tspace2 :TEXCOORD5; // вторая строка матрицы
					float3 tspace3 : TEXCOORD6;// третья строка матрицы 
				};
				v2f vert(appdata v)
				{
					v2f o;
					o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
					o.uv = v.uv;
					o.normUV = v.normUV;
					o.worldNormal = mul(unity_WorldToObject, v.normal);
					o.worldPos = mul(unity_ObjectToWorld, v.vertex);
					float3 worldTangent = mul(unity_ObjectToWorld, v.tangent); // получаем касательную в мировых коор-х
					float3 worldBitangent = cross(o.worldNormal, worldTangent); // получаем bitangent путем векторного перемножения нормали и косательной
					// Строим матрицу перехода 
					o.tspace1 = float3(worldTangent.x, worldBitangent.x, o.worldNormal.x); // tangent.x, bitangent.x, normal.x
					o.tspace2 = float3(worldTangent.y, worldBitangent.y, o.worldNormal.y); // tangent.y, bitangent.y, normal.y
					o.tspace3 = float3(worldTangent.z, worldBitangent.z, o.worldNormal.z); // tangent.z, bitangent.z, normal.z

					return o;

				}

				float4 frag(v2f v) : SV_Target
				{ 
					float4 col = tex2D(_Texture,v.uv);
					float3 tangentNormal = UnpackNormal(tex2D(_Normal_map,v.normUV*_Normal_map_ST.xy+_Normal_map_ST.zw)); // получаем нормаль в локальном пространстве
					float3 worldNormal;
					// Пошагово перемножаем каждый компонент нормали с столбцом матрицы 
					worldNormal.x = mul(tangentNormal, v.tspace1);
					worldNormal.y = mul(tangentNormal, v.tspace2);
					worldNormal.z = mul(tangentNormal, v.tspace3);
					worldNormal = normalize(worldNormal);
					// Делаем освещение по фонгу 
					float3 viewDir = normalize(_WorldSpaceCameraPos - v.worldPos);
					float3 l; // направление света 
					float3 atten; // затенение 
					if (_WorldSpaceLightPos0.w == 0)
					{
						l = normalize(_WorldSpaceLightPos0.xyz);
						atten = 1.0;
					}else{
						l = _WorldSpaceLightPos0.xyz - v.worldPos;
						atten = 1 / length(l);
						l = normalize(l);
					}
					float3 diff = atten * col * max(0.0, dot(l, worldNormal)); 
					float3 spec = float3(0.0, 0.0, 0.0);
					if (dot(l, v.worldNormal) > 0.0)
					{
						float3 refl = reflect(-l, worldNormal);
						spec = atten * _SpecColor * col * pow(max(0.0, dot(refl, viewDir)), _Shininess);
					}
					
					return float4(diff + spec, 1.0);

				}

			ENDCG
		}
		Pass
		{
			Blend DstAlpha OneMinusSrcAlpha
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
		uniform sampler2D _xgmTexture;
		uniform float4 _xgmTexture_ST;
		uniform float4 _xgmColor;
		struct appdata // Vertex Input structure
		{
			float4 vertex:POSITION;
			float2 xgmUV:TEXCOORD0;
		};
		struct v2f  // Vertex output structure
		{
			float4 pos:SV_POSITION;
			float2 xgmUV:TEXCOORD0;
		};

		v2f vert(appdata v)
		{
			v2f o;
			o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
			o.xgmUV = v.xgmUV;
			return o;

		}
		float4 frag(v2f v) :SV_Target
		{
			float4 col = tex2D(_xgmTexture,v.xgmUV * _xgmTexture_ST.xy + _xgmTexture_ST.zw);
			return ((1 - col)*(_xgmColor.r*0.5*_xgmColor));
		}
			ENDCG
		}
	}
}

-

Заключение

!Ну! вот и всё, по крайней мере, с основами вертексных и фрагментных шейдеров мы закончили. Если вы дошли до этого этапа и всё(-хотябы половину-) поняли, поздравляю!!! Этот способ(вертексный и пиксельный) написания шейдеров является самым "честным". Если вы освоили его, то вы можете без особого труда писать шейдеры где угодно, а не только в юнити. Следующий вид шейдеров – поверхностный(surface). И процесс написания самих этих шейдеров, тоже, несколько поверхностный, хотя, точнее будет сказать, что за нас половину работу выполнят движок. Это удобно, но для обучения, для понимания процессов, происходящих в «закулисье» , нужно научится писать пиксельные и фаргментные шейдеры.
Еще хотелось бы сказать, что если вы интересуетесь, как происходит перемножение матриц и векторов, как можно перевести вектор из одной системы в другую, и вообще, как использовать линейную алгебру в геймдеве, то рекомендую прочитать вот эту вот статью

Если есть вопросы, буду рад ответить )

Удачи!



Благо все эти преобразования в шейдере мы можем не делать. Для этих целей можно воспользоваться встроенной фунецией ?UnpackNormal?С этим разобрались, двигаемся дальше!

Вот тут вот странно с форматирование

?UnpackNormal?С
Почему кодируем только 2 компонента ?

Аа... так вот в чем косяк то был с моей реализацией... Я считал, что там нормали закодированы как есть x, y, z

Статья шикарная, спасибо!

alexprey:

Вот тут вот странно с форматирование

Спасибо, сейчас поправлю.) Статья преимущественно печаталась ночью, т.к. времени сейчас маловато, так что есть там пара заминок.

Статья очень годная получилась.
Работа с картами нормалей в SC2 и Юнити отличается, и сразу вкурить не вышло.
Спасибо за информацию!


Будет ли что-то кроме шейдеров?
Анимирование, например. 2Д и 3Д.

Jusper:

Будет ли что-то кроме шейдеров?
Анимирование, например. 2Д и 3Д.

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