Предыстория
Вышла книга Ламмерса на русском, астрологи предсказывают…
На конференции DevGAMM я купил задорого книгу Кенни Ламмерса в которой впоследствии расписались: Симонов, Галёнкик и Придюк. Вальяжно за два вечера я-таки добил её до середины и решил: собрать всё то что там написано в начале, переварить, нарисовать картинок и написать статью.

Статья предназначена для совсем новичков которые с трудом код на C# из уроков копируют, по этому я не буду углубляться в теорию которая и так уже описана. За место этого мы будем решать практические задачи и узнаем что шейдеры нужны не только что бы: «Всё сверкало и блестело».
Введение
Что такое шейдер? Это такая крутая программа которая исполняется на видеокарте. Круто да? А вот нет, в неумелых руках один кривой шейдер способен просадить вам FPS в нули, а может даже и отмотает время назад. Шейдер тоже не целостный, он ещё и исполняется на пикселях (Пиксельный шейдер) и на вертексах (Вертексный шейдер). То есть действительно для каждого пикселя вашего растра вызовется небольшая программа которая что-то там посчитает, если мы там ещё будем циклами крутить и if'ов наставим — будет совсем больно. Для этого Дамблдор придумал запретить ученикам
Тот чьё имя нельзя называть
В Unity3D сделали эдакую обёртку над HLSL и CG(шейдерные языки) — Shader Lab. Она позволяет не писать новые шейдеры под каждое API и предоставляет множество всего вкусного. Но Shader Lab всё-таки обёртка и внутри мы всё равно пишем на одном из этих языков, исторически так сложилось что почти все пишут на CG. Также Unity3D сама закидывает большую часть информации в шейдер так-что нам нужно просто описать что мы хотим а там движок разберётся.
Surface Shader — это такой функционал который абстрагирует нас от пиксельных и вертексных шейдеров и мы работаем с поверхностью. То-есть просто говорим цвет, силу отражения и нормаль. Это всё дело с компилируется в вертексные и пиксельные функции. Очень мощный инструмент который позволяет писать шейдеры легко и просто, вот даже я смог.
Пишем первый шейдер
В итоге у нас получится это:

Да это терраин, да он какой-то грязный. Суть в чём:
- Мы берём меш
- Берём карту высот
- Текстурируем меш по этой карте
В итоге мы получаем загрязнение склонов. Это очень простой пример в котором мы познакомимся с основными возможностями и задачами которые решают шейдеры. Я буду комментировать код:
Shader "Custom/HeightMapTexture" { //Имя шейдеры при выборе его на материале
Properties { //Блок с параметрами для Unity3D
_HeightTex ("HeightMap Texture", 2D) = "white" {} //Имя параметр, описание для редактора и тип. Далее идёт значение по умолчанию
_GrassTex ("Grass Texture", 2D) = "white" {} //Тип 2D указывает что тут обычная текстура
_RockTex ("Rock Texture", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" } //Теги для рендеринга
LOD 200
CGPROGRAM //Говорим что мы шпрехаем на CG и начинаем писать на нём
#pragma surface surf Lambert //Директива для доп. данных, таких как: Функция Surface шейдера и модель освещения.
sampler2D _GrassTex; //Указываем переменные в которые будут переданные данные из блока Properties имена и типы желательно указывать такие же
sampler2D _RockTex;
sampler2D _HeightTex;
struct Input { //Структура для входных данных для вызова шейдера, заполняется автоматически нам нужно только указать что хотим
float2 uv_GrassTex; //uv для текстур например, позволит настраивать тайлинг и оффсет из редактора
float2 uv_RockTex;
float2 uv_HeightTex;
};
void surf (Input IN, inout SurfaceOutput o) { //Собственно функция сурфейс шейдера
//Получаем из текстур цвета пикселей по UV
float4 grass = tex2D(_GrassTex, IN.uv_GrassTex); //Трава
float4 rock = tex2D(_RockTex, IN.uv_RockTex); //Камень\Грязь
float4 height = tex2D(_HeightTex, IN.uv_HeightTex); //Карта высот
o.Albedo = lerp(grass, rock, height); //Интерполируем по карте высот цвета травы и камня и получаем итоговый цвет
//И помещаем его в структуру SurfaceOutput которая нужна для передачи результата работы :)
}
ENDCG
}
FallBack "Diffuse" //Если что-то не так взять обычный дифуз
}
Вроде выглядит сложно, но ничего Гарри же смог патронусом дементров отгонять, а мы что не сможем пиксели интерполировать? Сейчас расскажу зачем мы это всё делаем и код станет чуточку понятней.
- Это карта высот. Мы берём из неё цвет пикселя, она чёрно белая так-что мы можем интерпретировать его как высоту. Допустим 1 это абсолютно высоко а 0 это абсолютно низко (даже ниже чем конверсия в твоей игре). Исходя из этого мы можем делать такие вещи как: Если 1 то берём полностью текстурку камня а если 0 травушки. Если будет серый то 50\50 и т.д Как видите по карте, высот у нас много так-что всё ложится мягко как в объятья Хагрида.

- lerp() — это такая функция для интерполяции, в первых двух аргументах мы указываем два значения между которыми будет что либо выбираться а во втором на каких основаниях. Туда мы могли бы передать одну из компонент пикселя, допустим только r но в данном случае у нас передаётся всё сразу. Потом расскажу почему так делать ненужно.
- tex2D() — Берём цвет пикселя с текстуры по UV. Очень скучно. UV мы получаем из структуры Input, юнити за нас позаботилась и туда всё положила. Для анимации текстуры мы можем допустим смещать UV по синусоидальному времени.
Понятней стало? Если нет, то вам срочно нужно покушать яблочного пирога а потом вернуться. В 70% это работает. Ах да, создаём шейдер там же где и скрипты. Потом вешаем его на материал, и материал суём в настройках терраина.

UV
Расскажу немного про UV, а то UV то UV сё — а что за зверь — непонятно.
В простом случае UV координата просто соответствует координате на объёкте. UV координаты лежат в пределе от 0 до 1. При таком раскладе текстура просто наложится на объект 1 к 1, местами растянется — но нам не страшно. Мы можем указать UV для каждой вершины и у нас получится развёртка, когда текстура ложится частями. Я конечно не эксперт но вроде Unity3D сама берёт информацию о развёртке из меша и передаёт его в шейдер так-что нам об этом думать пока-что не нужно.
Как сделать красиво и не подать виду. Или используем Normal map
Normal map — это такая текстура в rgb которой закодированы векторы. То есть в нашем трёхмерном пространстве обычный вектор состоит из трёх компонент x, y и z. Который как раз умещаются в компоненты текстуры. Поэтому она выглядит так странно когда мы смотрим на неё как на обычную текстуру, там на самом деле мозгошмыги сидя��.
Используя эту текстуру при рисование наших пикселей мы можем брать вектор из пикселя и преломлять по нему свет получая эффект объёмности. Звучит сложновато но используя колдунство поверхностных шейдеров и библиотеку Unity3D реализация этой задачи весьма тривиальна.
До

После

Шейдер:
Shader "Custom/BumpMapExample" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Bump map", 2D) = "bump" {} //Указываем что это обычная текстура
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Lambert
sampler2D _MainTex;
sampler2D _BumpMap;
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
};
void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); //Магия
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
Тут всё также как и в прошлом только теперь мы берём пиксель и передаём его в функцию UnpackNormal, она сама достанет нормаль и мы просто заносим её в структуру SurfaceOutput. Далее нормаль в функции освещения займёт своё место и там уже решат что с ней делать. К слову без источника света Normal Map'инг работать не будет.
Три непростительных заклятия
Немного про то как делать правильно:
- Собирать текстуры в одну. Допустим у нас есть карта высот, туда в пиксель мы можем упаковать аж 4 значения, для высоты нам достаточно только одного. То есть мы можем собирать несколько текстур в одну, раскидывая их по каналам. Полезная практика настоящих волшебников
- Часть логики можно и нужно переносить на скрипты. многое достаточно рассчитывать только каждый кадр а не каждый пиксель. Шейдеры помогают но не стоит всё делать только ими.
- Не использовать условные операторы, это очень нагружает систему и пытайтесь искать обходные пути.
- Всё что можно запечь заранее в текстуру, пеките. Освобождайте шейдер от лишней работы, взять данные из текстуры намного быстрее чем просчитать их заново.
Заключение
Это конец первой части. Дальше я хочу ещё рассказать про спекуляр, отображения с помощью CubeMap и о том как держать 300 анимированых юнитов в кадре на айпаде :)
Я очень надеюсь что все замечания по статье будут выражены максимально конструктивно для того что бы я и другие новички смогли обучаться. Я не считаю себя профессионалом в этом деле но имею некий опыт, и знаю сколько тонкостей в этом есть. Я очень надеюсь на помощь Хабрасообщества. Я намеренно упустил рассказ об шейдерных моделях и тонкостях в настройках тегов и прочего — для этого есть ещё не законченный материал в котором будем это разбирать.
