Будем разбираться в шейдерах
Вступление
Всем привет! Сегодня хочется рассказать про такую интересную вещь в 3D как шейдеры. Спойлер - это будет небольшая статья, здесь не будет много теории, в основном будем рассматривать написание шейдеров для Unity на основе GLSL.
Шейдер - компьютерная программа, предназначенная для исполнения на GPU. Шейдеры составляются на одном из специализированных языков программирования и компилируются в инструкции для графического процессора.
-- Википедия
То есть, для отображения любого 3D объекта необходимо иметь шейдер. При просчёте шейдера учитывается огромное количество информации, необходимой для корректного отображения: вершины, текстуры, освещённость, и так далее.
Есть множество типов шейдеров, вот некоторые из них:
Вершинные шейдера (vertex shaders)
Фрагментные (пиксельные) шейдера (fragment shaders)
Меш шейдера (mesh shaders)
Вычислительные шейдера (compute shaders)
Рассмотрим их по порядку: В вершинном шейдере атрибуты вершины обрабатываются для получения преобразованных атрибутов. К атрибутам могут относиться координаты вершины, цвет и многое другое. Вершинные шейдеры широко используются для таких задач как изменение текстурных координат (искажение), деформации и анимирования объектов, стилизация освещения и скининг. Фрагментные шейдеры отвечают за просчёт цвета одного пикселя. То есть, если вершинный шейдер вызывается для каждой вершины, то фрагментный - для каждого пикселя. Для просчёта цвета пикселя могут использоваться огромное количество атрибутов и переменных. Меш шейдеры - новый тип шейдеров, который объединяет в себе вершинный шейдер и примитивную обработку. Основная цель меш шейдера - добавить гибкости и производительности к геометрическому конвееру. Вычислительные шейдеры никак не относяться к визуализации 3D объектов. Вычислительные шейдеры выполняют на GPU массивные примитивные вычисления. основной профит в данном типе шейдеров - скорость обработки и высокая степень параллелизма.
Практика
Итак, предлагаю перейти к практике и написать простой шейдер. Будем писать unlit-шейдер. Unlit-шейдер это шейдер, который при просчёте объекта не учитывает освещение.
Полный код шейдера
Shader "Chernov/Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, -i.uv);
//UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Давайте рассмотрим его подробнее: Начинается шейдер с ключевого слова Shader, после чего в кавычках указывается имя шейдера. Далее шейдер для Unity состоит из нескольких частей - Properties и SubShader. В блоке Properties описываются переменные, которые будут отображены в инспекторе. Формат записи следующий: _variableName("Name in inspector", type) = defaultValue
к примеру, переменная текстуры будет выглядеть следующим образом:
_MainTex ("Texture", 2D) = "white" {}
а переменная для цвета вот так:
_SpecColor ("Specular color", color) = (1., 1., 1., 1.)
Доступные типы данных
Int = number
Float = number
Range(min,max) = number float в промежутке от min до max
Vector = (numer,number,number,number) x,y,z,w
Color = (numer,number,number,number) r,g,b,a
2D = "defaulttexture" {} 2D текстура
Cube = "defaulttexture" {} Кубическая текстура
После блока с переменными, обязательно должен быть блок SubShader. Их может быть несколько. В этом блоке шейдера определяется логика работы шейдера, в том числе логика на языке GLSL. Шейдер может содержать несколько сабшейдеров с различной логикой для различных видеокарт. Начинается сабшейдер с указания тегов.
Tags { "RenderType"="Opaque" }
В данном случае мы указываем что объект следует отрисовывать как непрозрачный. Далее по коду следует блок Pass, или проход шейдера. Внутри данного блока будут писаться вертексные и фрагментные части шейдера. Ключевые слова CGPROGRAM
и ENDCG
указывают блок шейдера на языке GLSL. Следовательно CGPROGRAM открывает блок кода, а ENDCG, соответственно, закрывает его. Далее идут две директивы:
#pragma vertex vert
#pragma fragment frag
которые указывают, что в качестве вертексного шейдера выступает функция с названием vert, а в качестве фрагментного - функция с именем frag.
Вот этой строчкой #include "UnityCG.cginc"
мы подключаем библиотеку со вспомогательными и наиболее полезными функциями. Подробнее про это библиотеку можно прочитать вот тут.
Далее мы указываем данные, которые нам будут необходимы для нашего шейдера.
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
appdata - данные, которые нам будут нужны для вертексного шейдера. В данном случае для просчёта атрибутов вершин нам необходимы координаты вершины и uv-координаты вершины в текстуре.
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f - данные, которые потребуются для фрагментного шейдера. В данном случае uv-координаты текстуры и позиция вершины. Далее следует блок переменных:
sampler2D _MainTex;
float4 _MainTex_ST;
Эти переменные будут использоваться внутри программы на языке GLSL. Для корректного маппинга переменных из блока Properties в переменные GLSL необходимо соблюдать нейминг. Названия переменных должны совпадать.
Итак, разобрав множество нюансов, мы наконец-то подошли к реализации логики шейдера. Рассмотри вершинную часть шейдера:
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
В первую очередь стоит обратить внимание на то, что вертексный шейдер - это функция с возвращаемым типом, и этот тип - тип данных, который ожидает фрагментный шейдер. o.vertex = UnityObjectToClipPos(v.vertex)
- преобразует точку из пространства объекта в пространство отсечения камеры. o.uv = TRANSFORM_TEX(v.uv, _MainTex)
- преобразует uv-координаты вершины в uv-координаты пикселя. Вроде как ничего сложного.
Теперь рассмотрим фрагментный шейдер:
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, -i.uv);
return col;
}
Сразу же обращаю внимание на то, что фрагментный шейдер должен вернуть цвет пикселя, поэтому возарщаемый формат у фрагментного шейдера - fixed4 (цвет RGBA с низкой точностью). Поскольку функция возвращает только одно значение, семантика указывается в самой функции: SV_Target
. fixed4 col = tex2D(_MainTex, -i.uv)
- вот тут мы извлекаем из текстуры пиксель, соответствующий uv-координатам. Так как шейдер у нас Unlit, то более никаких преобразований мы не предпринимаем. Вот и всё. Мы разобрали первый шейдер.
На этом пока что всё. Пишу первый раз, поэтому буду рад конструктивной критике. В дальнейшем планируется ещё несколько частей. Разберём амбиентные шейдеры, шейдеры воды, ткани, вычислительные шейдеры и много чего другого интересного.
Алексей Чернов
Team Lead at Program-Ace
Комментарии (10)
Chaos_Optima
11.10.2021 12:46+1Белый текст на светло-жёлтом фоне, вы специально хотели сделать больно глазам или вам правда нравится подобное сочетание?
Есть множество типов шейдеров, вот некоторые из них:
Вы конечно молодец, но mesh шейдер появился совсем недавно и это шейдер который используется крайне редко, почему для примеры вы выбрали именно его? А не классический набор, геометрический, хул и домэйн (можно в принципе просто теселяционные вместо хул и домэйн).
osharper
12.10.2021 13:38+2интересно, что дальше напишете, все хочу погрузиться в шейдеры, но все кажется, что это какая-то магия. Статья понравилась достаточно простым изложением, но некоторые вещи так и не объяснены. Например вот у вас в коде
float4 vertex : POSITION;
, ок, первое тип поля, второе имя, а после двоеточия это что? :)Ну и есть ощущение, что все это кажется магией потому что по какому-то негласному соглашению при написании шейдеров все забывают макконелла и прочие базовые советы про то, как выглядит хороший код, и начинаются всякие
frag (v2f i)
иcol
с уроков информатики вместо какого-нибудьpixelColor
(v2f
не смог расшифровать, честно; vector to fragment? vector to float? vertex to fragment?). Есть какая-то причина в игнорировании правил о семантичности названия переменных/структур и прочего?o.vertex = UnityObjectToClipPos(v.vertex)
- преобразует точку из пространства объекта в просторанство отсечения камеры.o.uv = TRANSFORM_TEX(v.uv, _MainTex)
- преобразует uv-координаты вершины в uv-координаты пикселя. Вроде как ничего сложного.а там еще одна строчка есть, что она делает? из всего сказанного мне так и не стало понятно, что же делает шейдер из статьи. ничего? просто показывает как вводится слой шейдеров в процесс рендеринга?
Chaos_Optima
12.10.2021 19:51+1ок, первое тип поля, второе имя, а после двоеточия это что? :)
Это метка input слота. Когда в коде формируешь вершинный буфер описываешь лэйаут вершины, и там помечаешь что есть что, метка POSITION отвечает в данном случае за позицию. Можно почитать тут https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics
Есть какая-то причина в игнорировании правил о семантичности названия переменных/структур и прочего?
Нет, просто плохой код ну или стиль.
а там еще одна строчка есть, что она делает?
Да и объяснение вида
преобразует uv-координаты вершины в uv-координаты пикселя
Тоже неверные, TRANSFORM_TEX применяет к текстурным координатам вершины тайлинг и офсет а не преобразует в координаты пикселя.
Ну и в принципе делать статью про шейдеры в рамках юнити с их макросами и готовыми библиотеками для шейдеров, как-то не очень красиво.
slepmog
Сервер — серверы.
Шейдер — шейдеры.
Север — севера.
Мастер — мастера.
ky0
Это совершенно неискоренимо, к сожалению, как и всякие «имели место быть» и «доброго времени суток». Чувство языка либо есть, либо ничего с ним не поделаешь.
pewpew
Масла, клапана
slepmog
Масла (существительное среднего рода в форме мн. ч.), клапаны (не входят в особую категорию).
major-general_Kusanagi
Лес => Лесы or Леса? :)
slepmog
Леса, так же как снега и луга: