Будем разбираться в шейдерах

Вступление

Всем привет! Сегодня хочется рассказать про такую интересную вещь в 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)


  1. slepmog
    11.10.2021 10:23
    +12

    Сервер — серверы.
    Шейдер — шейдеры.


    Север — севера.
    Мастер — мастера.


    1. ky0
      11.10.2021 11:17
      +2

      Это совершенно неискоренимо, к сожалению, как и всякие «имели место быть» и «доброго времени суток». Чувство языка либо есть, либо ничего с ним не поделаешь.


    1. pewpew
      11.10.2021 14:57
      +1

      Масла, клапана


      1. slepmog
        11.10.2021 15:28
        +2

        Масла (существительное среднего рода в форме мн. ч.), клапаны (не входят в особую категорию).


    1. major-general_Kusanagi
      12.10.2021 07:53

      Лес => Лесы or Леса? :)


      1. slepmog
        12.10.2021 10:05
        +2

        Леса, так же как снега и луга:


        Форма на , у некоторых слов может быть единственной или преобладающей: бокбока (боки только во фразеологическом сочетании руки в боки); веквека (веки только во фразеологических сочетаниях в кои-то веки, на веки вечные, во веки веков), глазглаза, луглуга, мехмеха, снегснега, стогстога, шелкшелка.


  1. Chaos_Optima
    11.10.2021 12:46
    +1

    Белый текст на светло-жёлтом фоне, вы специально хотели сделать больно глазам или вам правда нравится подобное сочетание?

    Есть множество типов шейдеров, вот некоторые из них:

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


  1. supremestranger
    11.10.2021 23:09
    -2

    Хорошая статья, пиши еще


  1. 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-координаты пикселя. Вроде как ничего сложного.

    а там еще одна строчка есть, что она делает? из всего сказанного мне так и не стало понятно, что же делает шейдер из статьи. ничего? просто показывает как вводится слой шейдеров в процесс рендеринга?


    1. 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 применяет к текстурным координатам вершины тайлинг и офсет а не преобразует в координаты пикселя.

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