Шейдера — это не сложно. Часть 1 — Вступление

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Вступление

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

Источник: https://habr.com/ru/post/582678/


Интересные статьи

Интересные статьи

Вторая часть поста про то, как сделать, чтобы продуктовая agile-команда выполнила задачу к определенному сроку, но при этом не изменила принципам работы по потоку. П...
В комментариях к предыдущей публикации hhba поделился статьёй, которая сама по себе достойна отдельной публикации, настолько там красивые решения приводятся. В дополнение к её обзору я постараюсь...
Продолжение. Начало в «Python как предельный случай C++. Часть 1/2». Переменные и типы данных Теперь, когда мы окончательно разобрались с математикой, давайте определимся, что в нашем языке дол...
Многабукафф о том, что квантовая химия думает о принципе работы мюонного катализа: как именно мюон понижает температуру требуемой плазмы. В двух частях. Суть первой части выражается одним пред...
Сегодня мы поговорим о перспективах становления Битрикс-разработчика и об этапах этого пути. Статья не претендует на абсолютную истину, но даёт жизненные ориентиры.