Всем привет. Сегодня я хотел бы задеть такую тему, как рендеринг и шейдеры в Unity. Шейдеры - простыми словами это инструкции для наших видео-карт, которые говорят, как правильно отрисовывать и трансформировать объекты в игре. Итак, welcome to the club buddy.
Перед прочтением
Статья является лишь кратким обзором-шпаргалкой. И в ней обрезаны некоторые моменты, поскольку чтобы полностью расписать все процессы, наверное придется написать целую книгу :D
Как работает рендеринг в Unity?
В текущей версии юнити у нас есть три различных пайплайна для отрисовки графики - Built-in, HDRP и URP. Прежде чем разбираться с рендерами, нам нужно понять саму концепцию пайплайнов, которые предлагает нам Unity.
Каждый из рендер-пайплайнов выполняет ряд этапов, которые выполняют более значимую операцию и формируют из этого полноценный процесс отрисовки. И когда мы загружаем на сцену модель (к примеру .fbx), до попадания на наши мониторы она проходит большой путь, словно передвигаясь из Москвы во Владивосток по разным дорогам.
Каждый рендер-пайплайн обладает своими свойствами, с которыми мы будем работать: свойства материалов, источники света, текстуры и все функции, которые происходят внутри шейдера, будут влиять на внешний вид и оптимизацию объектов на экране.
Итак, как же происходит этот процесс? Для этого мы должны поговорить о базовой архитектуре рендер-пайплайнов. Unity делит все на четыре этапа: прикладные функции, работа с геометрией, растеризация и обработка пикселей.
Hidden text
Обратите внимание, что это лишь базовая модель рендеринга в реальном времени, а каждый из этапов делится на потоки, о которых мы поговорим дальше.
Прикладные функции
Первое, что у нас происходит - это обработка стадий работы приложения (прикладные функции), которая начинается на CPU и происходит в пределах нашей сцены. Сюда можно включить:
Обработка физики и просчет столкновений;
Анимации текстур;
Ввод с клавиатуры и мыши;
Наши скрипты;
Здесь же наше приложение считывает хранящиеся в памяти данные для последующей генерации наших примитивов (треугольники, вершины и пр.), а в конце этапа стадии работы приложения все это отправляется на этап обработки геометрии для работы над преобразованием вершин, используя матричные трансформации.
Процессинг геометрии
Когда компьютер запрашивает через CPU у нашего GPU изображения, которые мы видим на экране, это производится в два этапа:
Когда состояние рендера настроено и пройдены этапы от обработки геометрии до обработки пикселей;
Когда объект отрисовывается на экране;
Фаза обработки геометрии происходит на GPU и отвечает за обработку вершин нашего объекта. Эта фаза делится на четыре подпроцесса, а именно: вершинный шейдинг, проекция, клиппинг и отображение на экране.
Когда наши примитивы были успешно загружены и собраны на первом прикладном этапе, они отправляются на этап вертексного шейдинга, у которого есть две задачи:
Просчитать позицию вершин у объекта;
Преобразовать положение в другие пространственные координаты (с локальных на мировые, как пример), для того чтобы их можно было отрисовать на экране;
Также во время выполнения данного этапа мы можем дополнительно выбрать свойства, которые будут нужны для следующих этапов отрисовки графики. Сюда входят нормали, тангентсы, а так же UV-координаты и другие параметры.
Проецирование и клиппинг работают как дополнительные этапы и зависят от настроек камеры на нашей сцене. Обратите внимание, что весь процесс рендеринга производится относительно Camera Frustum (поля обзора).
Проекция будет отвечать за перспективное или ортографическое отображение, а клиппинг позволяет обрезать лишнюю геометрию вне поля обзора.
Растеризация и работа с пикселями
Следующий этап работы рендеринга - растеризация. Он заключается в том, чтобы найти в нашей проекции пиксели, соответствующие нашим 2D координатам на экране. Процесс поиска всех пикселей, которые заняты экранным объектом и называется растеризацией. Этот процесс можно рассматривать как шаг синхронизации между объектами в нашей сцены и пикселями на экране.
Для каждого объекта на экране выполняются следующие этапы:
Настройка треугольника - отвечает за генерацию данных по нашим объектам и передачи для обхода;
Обход треугольника - перечисляет все пиксели, которые входя в группу полигона. В данном случае эта группа пикселей называется фрагментом (fragment);
Далее следует последний этап, когда мы собрали все данные и готовы к выводу пикселей на экран. В этот момент запускается фрагментный шейдер (еще известный как пиксельный шейдер), который отвечает за видимость каждого пикселя. В основном он отвечает за цвет каждого пикселя для вывода на экране.
Forward и Deferred шейдинг
Как мы уже знаем, у Unity есть три вида пайплайнов рендеринга: Built-In, URP и HDRP. С одной стороны у нас есть Built-In (самый старый вид рендера, соответствующий всем критериям Unity), а с другой более современные, оптимизированные и гибкие пайплайны HDRP и URP (называемые Scriptable RP).
Каждый из видов рендер-пайплайна имеет свои пути для обработки графики, которые соответствуют набору операций, необходимых для прохождения от загрузки геометрии до её отрисовки на экране. Это позволяет нам графически обрабатывать освещенную сцену (например, сцену с направленным светом и ландшафтом).
Примерами путей отрисовки могут быть прямой рендеринг (forward path), отложенный шейдинг (deferred path), а также устаревшие (legacy deferred и legacy vertex lit). Каждый из них поддерживает определенные возможности, ограничения и обладает своей производительностью.
В Unity по умолчанию рендеринг производится прямым путем (forward path). Это связано с тем, что он поддерживается наибольшим количеством видео-чипов, однако обладает своими ограничениями на освещение и другие возможности.
Hidden text
Обратите внимание, что URP поддерживает только прямой рендеринг (forward path), в то время как HDRP обладает большим выбором и может сочетать как прямой путь рендеринга, так и отложенный.
Чтобы лучше понять эту концепцию, нам стоит рассмотреть пример, когда у нас есть некий объект и прямое освещение (directional light). То, как будут взаимодействовать эти объекты и определяет наш путь отрисовки (модель освещения).
Также на результат работы будут влиять:
Характеристики материала;
Характеристики источников освещения;
Базовая модель освещения соответствует сумме трех различных свойств, таких как: ambient color, diffuse reflection и specular reflection.
Расчет освещения выполняется в шейдере, он может быть выполнен на вершину или на фрагмент. Когда освещение рассчитывается по вершинам, это называется вершинным освещением (per-vertex lighting) и выполняется на этапе вершинного шейдера, аналогично, если освещение рассчитывается по фрагментам, то оно называется per-fragment или per-pixel shader и выполняется на этапе фрагментного (пиксельного) шейдера.
Вершинное освещение работает гораздо быстрее пиксельного, однако нужно учитывать то, что для достижения красивого результата у ваших моделей должно быть большое количество полигонов.
Матрицы в Unity
Итак, вернемся к нашим этапам отрисовки, а точнее на этап работы с вершинами. Для их преобразования используются матрицы. Матрица - это список числовых элементов, которые подчиняются определенным арифметическим правилам и часто используются в компьютерной графике.
В Unity матрицы представляют собой пространственные преобразования, и среди них мы можем найти:
UNITY_MATRIX_MVP;
UNITY_MATRIX_MV;
UNITY_MATRIX_V;
UNITY_MATRIX_P;
UNITY_MATRIX_VP;
UNITY_MATRIX_T_MV;
UNITY_MATRIX_IT_MV;
unity_ObjectToWorld;
unity_WorldToObject;
Все они соответствуют матрицам четыре на четыре (4x4), то есть каждая из них имеет четыре строки и четыре столбца числовых значений. Примером матрицы может быть следующий вариант:
Как и говорилось ранее - наши объекты обладают двумя узлами (к примеру в некоторых графических редакторах они называются transform и shape) и оба отвечают за положение наших вершин в пространстве (объектном). Объектное пространство в свою очередь определяет положение вершин относительно центра объекта.
И каждый раз, когда мы будем изменять положение, поворот или масштаб вершин объекта - мы будем умножать каждую вершину на матрицу модели (в случае с Unity - UNITY_MATRIX_M).
Чтобы переводить координаты из одного пространства в другое и работать внутри него - мы будем постоянно работать с различными матрицами.
Свойства полигональных объектов
Продолжая тему работы с полигональными объектами, можно сказать, что в мире 3D графики каждый объект состоит из полигональной сетки. Объекты на нашей сцене обладают свойствами и каждый из них всегда содержит вершины, тагненсы, нормали, UV-координаты и цвет - все это вместе формирует Mesh. Всем этим и управляют такие подпрограммы, как шейдеры.
При помощи шейдеров мы можем получать доступ и изменять каждый из этих параметров. При работе с этими параметрами мы как правило будем использовать вектора (float4). Далее разберем каждый из параметров нашего объекта.
Подробнее про вершины
Вершины объекта, соответствующие набору точек, определяющих площадь поверхности в двухмерном или трехмерном пространстве. В 3D редакторах, как правило, вершины представлены как точки пересечения сетки и объекта.
Вершины характеризуются, как правило двумя моментами:
Они являются дочерними компонентами компонента transform;
Они имеют определенное положение в соответствии с центром общего объекта в локальном пространстве.
Это значит, что у каждой вершины есть свой компонент трансформаций, отвечающий за его размер, поворот и положение, а также атрибуты, которые указывают где эти вершины находятся относительно центра нашего объекта.
Нормали у объектов
Нормали по своей сути помогают определить, где у нас находится лицевая сторона у фрагментов нашего объекта. Нормаль соответствует перпендикулярному вектору на поверхности полигона, который используется для определения направления или ориентации грани или вершины.
Тангенсы
Обратившись к документации Unity, мы получим следующее описание:
Тангент - это вектор единичной длинны, следующий за поверхностью меша вдоль направления горизонтальной текстуры
Что я щас такое блин прочитал? Если простым языком - тангентсы следуют по U координатам в UV для каждой геометрической фигуры.
UV-координаты
Наверное, многие ребята смотрели на скины в GTA Vice City и, возможно, как и я даже пытались рисовать там что-то свое. И UV-координаты как раз таки связаны с этим. С их помощью мы можем расположить 2D текстуру на 3D объект, словно дизайнеры одежды, создавая выкройки, называемые UV-развертками.
Эти координаты действуют как опорные точки, которые управляют тем, какие тексели в текстурной карте соответствуют каждой вершине в сетке.
Область UV-координат равна диапазону между 0,0 (float) и 1,0 (float), где "ноль" означает начальную точку, а "1" - конечную точку.
Цвета вершин
Помимо позиций, вращения, размера, у вершин есть еще и свои цвета. Когда мы экспортируем объект из 3D-программы, она присваивает объекту цвет, на который нужно воздействовать, либо освещением, либо копированием другого цвета.
По умолчанию цвет вершин - белый (1,1,1,1), а цвета кодируются в RGBA. При помощи цветов вершин можно, к примеру, работать со смешиванием текстур, как показано на картинке выше.
Что же такое шейдер?
Итак, на основе всего того, что было описано выше - шейдер это небольшая программа, которая может быть использована для создания интересных эффектов в наших проектах. Внутри нее содержатся математические вычисления и списки инструкций (команд), которые позволяют обрабатывать цвет для каждого пикселя в области покрывающей объект на экране нашего компьютера, либо работать с трансформациями объекта (к примеру для создания динамической травы или воды).
Эта программа позволяет нам рисовать элементы (используя системы координат) на основе свойств нашего полигонального объекта. Шейдеры выполняются на GPU, поскольку он имеет параллельную архитектуру, состоящую из тысяч небольших, эффективных ядер, предназначенных для решения задач одновременно, в то время как CPU был разработан для последовательной серийной обработки.
Обратите внимание, что в Unity есть три типа файлов, связанных с шейдерами:
Во-первых, у нас есть программы с расширением ".shader", которые способны компилироваться в различные типы пайплайнов рендеринга.
Во-вторых, у нас есть программы с расширением ".shadergraph", которые могут компилироваться только в либо в URP, либо в HDRP. Кроме того, у нас есть файлы с расширением ".hlsl", которые позволяют нам создавать настраиваемые функции; обычно они используются в типе узла под названием
Custom Function, который находится в Shader Graph.
Существует также другой тип шейдеров с расширением ".cginc" - Compute Shader, который связан с ".shader" CGPROGRAM, а ".hlsl" связан с ".shadergraph" HLSLPROGRAM.
В Unity существует по крайней мере четыре типа структур, определенных для генерации шейдеров, среди которых мы можем найти комбинацию вершинного и фрагментного шейдера, шейдера поверхностей для автоматического расчета освещения и compute-шейдера для более продвинутых концепций.
Небольшой экскурс в язык шейдеров
Прежде чем приступить к написанию шейдеров в целом, мы должны принять во внимание, что в Unity существует есть три языка программирования шейдеров:
HLSL (High-Level Shader Language - Microsoft);
Cg (C for Graphics - NVIDIA) - устаревший формат;
ShaderLab - декларативный язык - Unity;
Мы же быстро пробежимся по Cg, ShaderLab и немного заденем HLSL. Итак...
Cg - это язык программирования высокого уровня, разработанный для компиляции на большинстве графических процессоров. Он был разработан NVIDIA в сотрудничестве с Microsoft и использует синтаксис, очень похожий на HLSL. Причина, по которой шейдеры работают с языком Cg, заключается в том, что они могут компилировать как HLSL и GLSL (OpenGL Shading Language), ускоряя и оптимизируя процесс создания материалов для видеоигр.
Все шейдеры в Unity (за исключением Shader Graph и Compute) написаны на декларативном языке под названием ShaderLab. Синтаксис этого языка позволяет отображать свойства шейдера в инспекторе Unity. Это очень интересно, поскольку мы можем манипулировать значениями переменных и векторов в реальном времени, настраивая наш шейдер для получения желаемого результата.
В ShaderLab мы можем вручную определить несколько свойств и команд, среди них блок Fallback, который совместим с различными типами пайплайнов рендеринга, которые существуют в Unity.
Fallback - это фундаментальный блок кода в мультиплатформенных играх. Он позволяет нам скомпилировать другой шейдер вместо того, который сгенерировал ошибку. Если шейдер сломался в процессе компиляции
Fallback возвращает другой шейдер, и графическое оборудование может продолжить свою работу. Это необходимо, чтобы нам не писать различные шейдера под XBox и PlayStation, а использовать единые шейдеры.
Базовые типы шейдеров в Unity
Базовые типы шейдеров в Unity позволяют нам создавать подпрограммы, которые будут использоваться для различных целей.
Разберем, за что отвечает каждый тип:
Standart Surface Shader - Этот тип шейдера характеризуется оптимизацией написания кода, который взаимодействует с базовой моделью освещения и работает только с Built-In RP.
Unlit Shader - Относится к первичной цветовой модели и будет базовой структурой, которую мы обычно используем для создания наших эффектов.
Image Effect Shader - Структурно он очень похож на Unlit шейдер. Эти шейдеры используются в основном в эффектах постобработки в Built-In RP и требуют функции "OnRenderImage()" (C#).
Compute Shader - Этот тип характеризуется тем, что выполняется на видеокарте и структурно сильно отличается от ранее упомянутых шейдеров.
RayTracing Shader - Экспериментальный тип шейдеров, позволяющий собирать и обрабатывать трассировку лучей в реальном времени, работает только с HDRP и DXR.
Blank Shader Graph - Пустой шейдер на основе графов, с которым вы можете работать без знаний языков шейдеров, вместо этого используя ноды.
Sub Graph - Подшейдер, который можно использовать в других шейдерах Shader Graph.
Структура шейдеров
Для анализа структуры шейдеров, достаточно создать простой шейдер на основе Unlit и проанализировать его.
Когда мы создаем шейдер в первый раз, Unity добавляет код по умолчанию, чтобы облегчить процесс компиляции. В шейдере мы можем найти блоки кода, структурированные таким образом, чтобы графический процессор мог их интерпретировать.
Если мы откроем наш шейдер, его структура будет выглядит похожим образом:
Shader "Unlit/OurSampleShaderUnlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags {"RenderType"="Opaque"}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler 2D _MainTex;
float4 _MainTex;
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;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Скорее всего, посмотрев на этот код, вы не поймете, что происходит в различных его блоках. Однако, чтобы начать наше исследование, мы обратим внимание на его общую структуру.
Shader "InspectorPath/shaderName"
{
Properties
{
// Здесь у нас находятся параметры шейдера
}
SubShader
{
// Здесь у нас конфигурируется сабшейдер
Pass
{
CGPROGRAM
// Здесь у нас расположена Cg программа - HLSL
ENDCG
}
}
Fallback "ExampleOfOtherShaderForFallback"
}
С текущим примером и его основной структурой становится несколько понятнее. Шейдер начинается с пути в инспекторе редактора Unity (InspectorPath) и имени (shaderName), затем свойства (например.
текстуры, векторы, цвета и т.д.), затем SubShader и в конце необязательный параметр Fallback для поддержки различных вариантов.
Таким образом, мы уже понимаем что, где и зачем начать писать.
Работа с ShaderLab
Большинство наших шейдеров, написанных в коде, начинаются с объявления шейдера и его пути в инспекторе Unity, а также его имени. Оба свойства, такие как SubShader и Fallback, записываются внутри поля "Shader" в декларативном языке ShaderLab.
Shader "OurPath/shaderName"
{
// Код шейдера будет здесь
}
И путь, и имя шейдера могут быть изменены по мере необходимости в рамках проекта.
Свойства шейдера соответствуют списку параметров, которыми можно манипулировать из инспектора Unity. Существует восемь различных свойств, как по значению, так и по полезности. Мы используем эти свойства относительно шейдера, который мы хотим создать или изменить, динамически или в рантайме. Синтаксис для объявления свойства следующий:
PropertyName ("display name", type) = defaultValue.
Где "PropertyName" означает имя свойства (например, _MainTex), "display name" задает имени свойства в инспекторе Unity (например. Texture), "type" указывает на его тип (например, Color, Vector, 2D и т.д.) и, наконец, "defaultValue" - это значение по умолчанию, присвоенное свойству (например, если свойство свойство является "Color", мы можем установить его как белый следующим образом (1, 1, 1, 1, 1).
Вторым компонентом шейдера является Subshader. Каждый шейдер состоит как минимум из одного SubShader для идеальной загрузки. Когда имеется более одного SubShader, Unity будет обрабатывать каждый из них и выбирать наиболее подходящий в соответствии с аппаратными характеристиками, начиная с первого и заканчивая последним в списке (к примеру для того, чтобы разделить шейдер под iOS и Android). Когда SubShader не поддерживается, Unity попытается использовать компонент Fallback, соответствующий стандартному шейдеру, чтобы аппаратное обеспечение могло продолжить выполнение своей задачи без графических ошибок.
Shader "OurPack/OurShader"
{
Properties { … }
SubShader
{
// Здесь будет конфигурация шейдера
}
}
Прочитать подробнее про параметры и сабшейдеры можно здесь и здесь.
Блендинг
Блендинг нужен нам для процесса смешивания двух пикселей в один. Блендинг поддерживается как в Built-In, так и SRP.
Блендинг происходит на этапе, который объединяет конечный цвет пикселя с его глубиной. Этот этап, который происходит в конце пайплайна рендеринга, после этапа фрагментного (пиксельного) шейдера, при выполнении
stencil-буфера, z-буфера и смешивания цветов.
По умолчанию это свойство не записано в шейдере, так как это необязательная функция и используется в основном при работе с прозрачными объектами, например, когда мы должны нарисовать пиксель с
низким уровнем непрозрачности перед другим (часто такое используется в UI).
Мы можем включить смешивание здесь:
Blend [SourceFactor] [DestinationFactor]
Подробнее о блендинге вы можете почитать здесь.
Z-Buffer и тест глубины
Чтобы понять обе концепции, мы должны сначала узнать, как работают Z-буфер (также известный как Depth Buffer) и тест глубины.
Перед началом работы мы должны учесть, что пиксели имеют значения глубины. Эти значения хранятся в буфере глубины, который определяет, идет ли объект перед или за другим объектом на экране.
С другой стороны, тестирование глубины - это условие, которое определяет, будет ли пиксель обновлен или нет в буфере глубины.
Как мы уже знаем, пиксель имеет назначенное значение, которое измеряется в цвете RGB и хранится в буфере цвета. Z-буфер добавляет дополнительное значение, которое измеряет глубину пикселя с точки зрения расстояния до камеры, но только для тех поверхностей, которые находятся в пределах его фронтальной области. Это позволяет двум пикселям быть одинаковыми по цвету, но разными по глубине.
Чем ближе объект к камере, тем меньше значение Z-буфера, и пиксели с меньшими значениями буфера перезаписывают пиксели с большими значениями.
Чтобы понять концепцию, предположим, что у нас есть камера и некоторые примитивы в нашей сцене, и все они расположены на оси пространства "Z".
Слово "буфер" относится к "пространству памяти", в котором данные будут временно храниться, поэтому Z-буфер относится к значениям глубины между объектами в нашей сцене и камерой, которые присваиваются каждому пикселю.
Мы можем управлять Depth-тестом, благодаря параметрам ZTest в Unity.
Culling
Это свойство, совместимое как c Built-In RP, так и в URP/HDRP, управляет тем, какая из граней многоугольника будет удалена при обработке глубины пикселя.
Что это значит? Вспомните, что многоугольный объект имеет внутренние грани и внешние. По умолчанию внешние грани видны (CullBack);
Однако мы можем активировать внутренние грани:
Cull Off - Отрисовываются обе грани объекта;
Cull Back - По умолчанию отображаются задние грани объекта;
Cull Front - Отрисовываются передние грани объекта;
Эта команда имеет три значения, а именно: Back, Front и Off. По умолчанию активна команда "Back", однако, как правило, строка кода, связанная с culling, не видна в шейдере в целях оптимизации. Если мы хотим изменить параметры, мы должны добавить слово "Cull" после которого следует режим, который мы хотим использовать.
Shader "Culling/OurShader"
{
Properties
{
[Enum(UnityEngine.Rendering.CullMode)]
_Cull ("Cull", Float) = 0
}
SubShader
{
// Cull Front
// Cull Off
Cull [_Cull]
}
}
Мы также можем динамически настраивать параметры Culling в инспекторе Unity через зависимость "UnityEngine.Rendering.CullMode", которая является Enum и передается в качестве аргумента в функции.
Использование Cg / HLSL
В нашем шейдере мы можем найти как минимум три варианта директив по умолчанию. Это директивы процессора и включаются в Cg или HLSL. Их функция заключается в том, чтобы помочь нашему шейдеру распознать и скомпилировать определенные функции, которые иначе не могут быть распознаны как таковые.
#pragma vertex vert - позволяет скомпилировать этап вершинного шейдера, называемый vert, в GPU как вершинный шейдер;
#pragma fragment frag - Директива выполняет ту же функцию, что и pragma vertex, с той разницей, что она позволяет этапу фрагментного шейдера под названием "frag" компилироваться в коде как фрагментный шейдер.
#pragma multi_compile_fog - В отличие от предыдущих директив, имеет двойную функцию. Во-первых, multi_compile относится к варианту шейдера, который позволяет нам генерировать варианты с различнымифункциональными возможностями в нашем шейдере. Во-вторых, слово "_fog" включает функциональные возможности тумана из окна Lighting в Unity, это означает, что если мы перейдем на вкладку Environment / OtherSetting, мы можем активировать или деактивировать опции тумана нашего шейдера.
Помимо этого мы можем подключать файлы Cg / HLSL в наш шейдер. Как правило мы делаем это, когда подключаем UnityCG.cginc, который в свою очередь включает в себя координаты тумана, позиции объекта для клиппинга, трансформации текстур, перенос и принятие тумана и многое другое, включая константы UNITY_PI.
Самое важное, что мы можем делать с Cg / HLSL - это написание непосредственных функций обработки вертексных и фрагментных шейдеров, использовать переменные этих языков и различные координаты, вроде текстурных (TEXCOORD0).
#pragma vertex vert
#pragma fragment frag
v2f vert (appdata v)
{
// Возможнность работы с вертексным шейдером
}
fixed4 frag (v2f i) : SV_Target
{
// Возможность работы с фрагментным шейдером
}
Подробнее о Cg / HLSL можно почитать здесь.
Shader Graph
Shader Graph - новое решение для юнити, позволяющая без знания шейдерного языка мастерить свои решения. Для работы с ним используются визуальные ноды (однако никто не запрещает сочетать их с шейдерным языком). Shader Graph предпочтительно работает только с HDRP и URP.
Мы должны учитывать, что при работе с Shader Graph версии, разработанные для Unity 2018 являются BETA-версиями и не получают поддержки, в то время как версии, разработанные для Unity 2019.1+ являются активно совместимыми и получают поддержку.
Еще одна проблема заключается в том, что весьма вероятно, что шейдеры, созданные с помощью этого интерфейса, могут не компилироваться правильно в различных версиях. Это происходит потому, что новые функции добавляются в каждом обновлении.
Итак, является ли Shader Graph хорошим инструментом для разработки шейдеров? Конечно. И с ним может разобраться не только графический программист, но и технический дизайнер или художник.
Чтобы создать граф, достаточно выбрать нужный нам тип в редакторе Unity:
Перед началом работы, сделаем небольшое введение в вершинный/фрагментный шейдер на уровне Shader Graph.
Как мы видим, на этапе вершинного шейдера есть три определенных точки входа, а именно: Position(3), Normal(3), Tangent(3), как в шейдере Cg или HLSL. Если сравнивать с обычным шейдером, то это означает, что Position(3) = POSITION[n], Normal(3) = NORMAL[n] и Tangent(3) = TANGENT[n].
Почему Shader Graph имеет три измерения, а Cg или HLSL - четыре?
Вспомним, что четвертое измерение вектора соответствует его компоненту W, который в большинстве случаев равен "единице или нулю". Когда W = 1, это означает, что вектор соответствует положению в пространстве или точке. В то время как, когда W = 0, вектор соответствует направлению в пространстве.
Итак, чтобы настроить наш шейдер, первое, что мы сделаем, это перейдем в редактор и создадим два параметра: цвет - _Color, и Texture2D - _MainTex.
Чтобы создать связь между свойствами ShaderLab и нашей программой, мы должны создать переменные в поле CGPROGRAM. Однако, в Shader Graph этот процесс происходит иначе. Мы должны перетащить свойства, которые мы хотим использовать в область работы с нодами.
Все, что нам нужно сделать, чтобы текстура типа Texture2D работала в сочетании с нодой Sample Texture 2D, это соединить выход свойства _MainTex с входом типа Texture(T2).
Чтобы перемножить обе ноды (с цветом и текстурой), мы должны просто вызвать ноду Multiply и передать оба значения в качестве точки входа. Наконец, output у цвета (коэффициент) выходящий из множителя нам нужно прокинуть в Base Color, найденному на этапе фрагментного шейдера. Сохраняем шейдер - и все готово. Наш первый шейдер готов.
Также мы можем обратиться к общей настройке графа, разделенной на две секции под названием Node и Graph, которые обладают настраиваемыми свойствами, позволяющими изменять цветопередачу. Мы можем найти опции блендинга, обводки, и альфа-клиппинга и др. Кроме того, мы можем настроить свойства нодов в нашей конфигурации Shader Graph.
Сами же ноды предоставляют аналоги тех или иных функций, которые мы пишем в ShaderLab. Как пример, код функции Clamp:
void Unity_Clamp_float4(float4 In, float4 Min, float4 Max, out float4 Out)
{
Out = clamp(In, Min, Max);
}
Превратиться в ноду:
Таким образом мы можем упростить себе жизнь и уменьшить время написание шейдеров за счет визуальных графов.
Итоги
Говорить на тему шейдеров можно очень много и очень долго, как и затрагивать сам процесс рендеринга. Здесь я не затронул шейдеры рейтрейсинга и Compute-Shading, капнул по шейдерным языкам поверхностно и описал процессы лишь с верхушки айсберга. Работа с графикой - это целые дисциплины, о которых в интернете можно найти тонны исчерпывающей информации, например:
Мануал по шейдерам от Unity;
Разбор рендеринга в играх;
Информация по языку HLSL;
Интересно было бы послушать о вашем опыте работы с шейдерами и рендерингом в рамках Unity, а так же услышать ваше мнение - что же лучше SRP или Built-In :-)
Спасибо за внимание!