Unity: Введение в шейдеры

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

Версия: C# 7.3, Unity 2019.3, Unity

Unity оказывает большую помощь разработчикам игр, в том числе предлагает шейдеры, чтобы упростить создание сложных визуальных эффектов и анимации. Вы можете продвинуться довольно далеко, просто разбираясь со стандартными шейдерами, которые поставляются с Unity. Однако вы можете улучшить визуальные эффекты в своих проектах, написав собственные шейдеры.

В этом руководстве вы узнаете:

  • Что такое шейдеры.
  • Как отображать цвета вершин.
  • Как анимировать в шейдерах.

Чтобы попрактиковаться, вы будете полировать графику сцены необитаемого острова с помощью пользовательских шейдеров. Загрузите файлы проекта, нажав кнопку «Скачать материалы урока» вверху этого урока. В примере проекта представлены модели и текстуры, так что вы можете сосредоточиться на шейдерах.

Примечание. Некоторые модели и текстуры, которые использует начальный проект, взяты с сайтов Sharegc.com и Textures.com.

В примере проекта используется Unity 2019.3, хотя все, что описано в этом руководстве, должно работать и в более старых версиях.

Примечание. Это урок среднего уровня сложности, предполагающий, что вы уже знаете основы работы с Unity. Если вы новичок в Unity, начните с урока Введение в Unity: начало работы.

Что такое шейдеры?

Компьютерная графика, особенно 3D-графика, использует множество различных видов информации для построения визуальных элементов: сетки, текстуры, источники света и так далее. Эта информация передается графическому оборудованию, которое затем обрабатывает изображение и выводит его на экран.

Рендеринг — это то, что программисты называют процессом создания изображения, а шейдеры — это короткие программы, которые визуализируют графические данные. То есть шейдер — это программа, которая принимает сетки, текстуры и так далее. В качестве входных данных и генерирует изображение в качестве выходных данных.

Как компьютеры визуализируют графику

Понимание типов шейдеров

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

Основным типом является пиксельный шейдер, названный по той очевидной причине, что он выводит пиксель. Другой распространенный термин — это фрагментный шейдер, поскольку каждый пиксель является фрагментом полного изображения.

Фрагментные шейдеры выводят цвет одного пикселя, который вычисляется на основе положения этого пикселя на многоугольнике. Программа запускается снова и снова, в основном параллельно, для генерации всех пикселей всех полигонов, обрабатываемых этим шейдером. Действительно, возможность одновременной обработки большего количества пикселей является важной частью того, как видеокарты ускоряют графику.

Есть также вершинные шейдеры, которые вы используете для вычисления положения вершин на изображении. Хотя входные данные представлены в трех измерениях, компьютеру необходимо определить, где появляются вершины в двух измерениях, прежде чем он сможет отобразить пиксели.

Вы можете добиться некоторых интересных визуальных эффектов, манипулируя данными, выводимыми с помощью вершинных шейдеров, но мы не будем рассматривать это в уроке.

Вычислительные шейдеры на самом деле ничего не обрабатывают, а представляют собой просто программы, работающие на видеооборудовании. Эти шейдеры являются недавним нововведением — более старое видеооборудование может даже не поддерживать их.

Как и в случае с фрагментными и вершинными шейдерами, вычислительный шейдер — это короткая программа, которую видеокарта запускает в массовом параллельном режиме. В отличие от других шейдеров, вычислительные шейдеры не выводят ничего визуального. Вместо этого они используют преимущества параллельной обработки на видеокартах для таких вещей, как майнинг криптовалюты. В этом руководстве вы не будете работать с вычислительными шейдерами.

Unity представляет еще один вид шейдеров — шейдер поверхности. Это шейдеры, которые выполняют расчеты освещения за вас, и вам нужно только написать код — помните, шейдер — это программа, — для обработки других свойств поверхности, таких как цвет и текстура.

Поверхностные шейдеры на самом деле являются фрагментными и вершинными шейдерами, но Unity генерирует код для обработки освещения. Это удобно, поскольку расчеты освещения очень сложны и часто стандартизируются для множества различных шейдеров.

И наконец, графические данные сообщают видеокарте, какой шейдер использовать с материалами. Когда вы будете изучать материалы, вы увидите меню вверху. Это меню выбирает, какой шейдер назначить материалу. Таким образом, когда графическое оборудование визуализирует многоугольник, оно запускает шейдер материала этого многоугольника.

Выбор из меню Shader окна Inspector в Unity

Хорошо, достаточно объяснений. Пришло время написать шейдеры!

Написание пользовательского шейдера

Если вы еще этого не сделали, откройте начальный проект, затем откройте RW / Scenes / SampleScene. Вы увидите заранее построенную сцену необитаемого острова с позиционированными моделями, которым назначены материалы.

Сцена песчанного острова в начале создания шейдеров в Unity

Существующая сцена не выглядит ужасной, но вы можете улучшить ее, по крайней мере, двумя способами: остров имеет твердые квадратные края, а вода полностью статична.

Вы можете решить обе проблемы с помощью пользовательских шейдеров. Однако, прежде чем решать эти проблемы, вам необходимо понять, как создавать шейдеры.

Для начала создайте ассет Surface Shader в папке Shaders, щелкнув правой кнопкой мыши и выбрав Create > Shader > Standard Surface Shader. Назовите его MyFirstShader.

Перейдите в папку Materials, выберите cartoon-sand и щелкните раскрывающийся список Shader в верхней части Inspector. Выберите Custom > MyFirstShader, чтобы переключить материал на этот шейдер.

Выбор MyFirstShader в качестве шейдера для песчанного материала в Unity

Пока что ничего особенного в сцене не изменится, потому что вы не изменили код шейдера по умолчанию. Тем не менее, материал песка на острове теперь использует ваш собственный шейдер, а не стандартный шейдер, встроенный в Unity.

Рассмотрение шаблона по умолчанию для настраиваемого шейдера

Дважды нажмите на ассет Shader, чтобы открыть его в редакторе кода, и изучите код.

Шейдер состоит из различных разделов внутри основного блока кода шейдера.

Весь код находится внутри блока шейдера в фигурных скобках с именем Custom/MyFirstShader. Эта запись просто сообщает Unity, что показывать при просмотре меню шейдеров материала. Глядя на разные блоки кода…

Properties

	Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
	}

В блоке Properties вверху вы объявляете различные определяемые пользователем свойства. Это значения, которые вы можете редактировать в инспекторе при выборе материала.

Подробное рассмотрение одной строки: сначала вы видите имя переменной, используемое в коде. Затем в круглых скобках вы определяете имя, которое будет отображаться в инспекторе. Также в круглых скобках указывается тип свойства: цвет, двухмерная текстура или диапазон чисел. Наконец, после знака = вы присваиваете свойству значение по умолчанию.

Блок SubShader

	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200

Блок кода Subshader — это то место, где находится большая часть кода шейдера. Первые две строчки в этом блоке объявляют идентифицирующие теги, распознаваемые Unity, котоыре устанавливают значение, используемое системой уровня детализации (LOD) Unity.

В этом случае идентифицирующие теги заявляют, что шейдер не является прозрачным. Технически может быть несколько блоков субшейдера, но в этом руководстве вы не будете углубляться в такой уровень сложности.

Блок CGPROGRAM

		CGPROGRAM
		// Physically-based standard lighting model,
                // and enable shadows on all light types
		#pragma surface surf Standard fullforwardshadows

		// Use Shader model 3.0 target to get nicer looking lighting
		#pragma target 3.0

Эти строчки кода декларируют несколько важных аспектов, которым нужно следовать. Во-первых, они указывают, что следующий код использует язык Cg. Для написания шейдеров существует несколько разных языков программирования. До этого момента все использовалось на языке Unity, ShaderLab.

Между тем, директивы #pragma устанавливают значения конфигурации. Подробно рассмотрим первый: поверхность сообщает компилятору шейдеров, что это шейдер поверхности. Опять же, помните, что другие типы включают фрагментные и вершинные шейдеры.

surf — это название основной функции затенения, представленной ниже. Standard объявляет желаемую модель освещения — другие модели освещения включают Lambert и Blinn-Phong, но стандартное физическое освещение выглядит лучше всего. fullforwardshadows активирует динамические тени для этого шейдера.

Переменная свойства MainTex

		sampler2D _MainTex;

Эта строчка объявляет переменную, соответствующую одному из свойств. Хотя это кажется немного избыточным, вы должны объявить здесь переменную, чтобы код Cg использовал это свойство.

Эти переменные могут быть одного из нескольких типов, включая sampler2D для изображения текстуры и fixed / half / float для чисел. Эти три числа являются числами возрастающей точности, и вы должны использовать наименьшую точность, которая работает.

Числовые значения могут иметь номер суффикса для создания вектора. Например, fixed4 обозначает четыре числа. Вы получаете доступ к значениям в векторе с помощью свойств .xyzw или .rgba.

Например, c.rgb в коде шейдера извлекает первые три числа из fixed4, вызываемого c.

Вы должны объявить все свойства как переменные в коде Cg. Вы можете увидеть другие имена свойств чуть позже в уроке. Почему в коде шаблона Unity они написаны отдельно друг от друга, а не в один единый список, остается загадкой, но это также не имеет особого значения.

Вводные данные и переменные свойства

		struct Input {
			float2 uv_MainTex;
		};

		half _Glossiness;
		half _Metallic;
		fixed4 _Color;

		// Add instancing support for this Shader. You need to check
                // 'Enable Instancing' on materials that use the Shader.
		// See https://docs.unity3d.com/Manual/GPUInstancing.html for
                // more information about instancing.
		// #pragma instancing_options assumeuniformscaling
		UNITY_INSTANCING_BUFFER_START(Props)
			// put more per-instance properties here
		UNITY_INSTANCING_BUFFER_END(Props)

Этот блок кода объявляет структуру данных с именем Input и перечисляет в ней значения. Единственными входными значениями в коде шаблона являются UV-координаты основной текстуры, но есть несколько входных значений, к которым шейдеры могут получить доступ. Графические данные передадут объявленные здесь входные значения в шейдер.

Главная функция шейдера

		void surf (Input IN, inout SurfaceOutputStandard o) {
			// Albedo comes from a texture tinted by color
			fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			// Metallic and smoothness come from slider variables
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	}

surf — это основная функция затенения, которую вы объявляете в строке #pragma выше. Первый параметр — это структура input, а другой параметр — это результат, в который функция записывает данные.

Вы заметите, что структура вывода имеет такие параметры, как .Metallic и .Smoothness, которые вы устанавливаете с помощью свойств шейдера.

Единственная строка в surf, которая не является прямым присвоением номера входа выходному значению, — это fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color ;.

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

FallBack шейдер

	FallBack "Diffuse"
}

После кода Cg вы объявляете резервный шейдер, хотя он и не требуется. Это указывает Unity использовать резервный шейдер, если графическое оборудование не может запускать какие-либо пользовательские субшейдеры, обычно потому, что это старая видеокарта, которая не поддерживает функции шейдера.

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

Добавление цвета вершин в шейдер поверхности

Первая проблема, которую вы хотели исправить в этой сцене, заключалась в том, что у острова были твердые квадратные края.

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

Есть много способов добиться этого в игре, но в одном простом подходе используется цвет вершин.

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

Координаты текстуры являются обычным дополнением, при этом координаты (обозначенные буквами UV вместо XY) предоставляют числа, которые вы используете при работе с текстурами. Что ж, цвет — это еще один вариант, предоставляющий числа, которые вы используете при расчете цветового вывода шейдера.

Например, если вы присвоите вершине красный цвет, то многоугольник, использующий эту вершину, будет иметь красный оттенок. Более того, если вершины многоугольника имеют разные цвета, то эти цвета интерполируются по граням многоугольника.

Цвета, интерполированные на грань треугольника

Этому островному мешу уже назначены цвета вершин, но вы не можете их видеть, потому что стандартный шейдер Unity не обрабатывает цвета вершин.

В частности, вершины под водой имеют более темный оттенок, а остальные вершины острова белые. Эти цвета создали бы приятный градиент по краю пляжа, если бы они были видны, поэтому ваша следующая задача — написать собственный шейдер, который обрабатывает цвет вершин.

Создание пользовательского шейдера для цвета вершин

Создайте новый шейдер поверхности и назовите его LitVertexColor. Установите материалу cartoon-sand этот шейдер как используемый, а затем откройте ресурс шейдера, чтобы отредактировать код.

Вам нужно всего лишь сделать два небольших дополнения для цвета вершины. Сначала добавьте в структуру Input следующий код:

struct Input {
	float2 uv_MainTex;
	float4 vcolor : COLOR; // vertex color
};

Далее включите vcolor в расчет цвета, который происходит в surf:

fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color * IN.vcolor;

И это все! Сохраните шейдер и вернитесь в сцену Unity. После компиляции шейдера вы увидите, как края острова сливаются под водой.

Сцена песчанного острова со смешанными краями

Анимация текстуры воды

Теперь песок выглядит намного привлекательнее! Между тем, вы обнаружили еще одну проблему, заключающуюся в том, что вода полностью статична.

Вы можете изменить это с помощью другого настраиваемого шейдера, хотя на этот раз вам не нужно освещение. Помните, что поверхностные шейдеры обрабатывают освещение за вас, поэтому на этот раз вы не будете создавать поверхностные шейдеры.

Создание неосвещенного шейдера

Вместо этого создайте Unlit Shader с помощью контекстного меню: Create > Shader > Unlit Shader.

Назовите новый ассет шейдер CartoonWater и откройте его в редакторе кода.

Первым делом обновите имя шейдера в самом верху.

Shader "Custom/CartoonWater"

Имя по умолчанию для этого шейдера — "Unlit / CartoonWater". Изменение этого имени на "Custom / CartoonWater" упрощает поиск ваших пользовательских шейдеров в меню.

Затем добавьте некоторые дополнительные свойства для шейдера воды. Новый блок Properties должен выглядеть так:

Properties
{
   _MainTex ("Texture", 2D) = "white" {}
   _Opacity ("Opacity", Range(0,1)) = 0.5
   _AnimSpeedX ("Anim Speed (X)", Range(0,4)) = 1.3
   _AnimSpeedY ("Anim Speed (Y)", Range(0,4)) = 2.7
   _AnimScale ("Anim Scale", Range(0,1)) = 0.03
   _AnimTiling ("Anim Tiling", Range(0,20)) = 8
}

По умолчанию Unlit Shader имеет только одно свойство текстуры _MainTex, поэтому вы добавили дополнительные свойства для управления как непрозрачностью воды, так и тем, как анимируется текстура поверхности.

Затем обновите раздел Tags и сразу после строки LOD 100 добавьте несколько новых параметров:

Tags { "RenderType"="Transparent" "Queue"="Transparent" }
LOD 100
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

Теги по умолчанию предполагают непрозрачный материал, поэтому вы добавили и настроили некоторые флаги для поддержки прозрачности. В частности, обратите внимание, что отключение ZWrite означает, что вода не будет закрывать другие объекты в буфере глубины, в то время как вы устанавливаете Blend для использования альфа-значений.

Теперь обратите внимание на раздел CGPROGRAM с его набором директив pragma.

Раньше вы работали с поверхностным шейдером, но теперь вы имеете дело с прямыми вершинными и фрагментными шейдерами. Таким образом, в то время как директива #pragma ранее объявляла функцию Surface Shading, на этот раз директивы #pragma объявляют функции Vertex и Fragment Shading.

Также обратите внимание на инструкцию #include. Unity предоставляет здесь библиотеку полезных функций (которые эта строка импортирует для использования в шейдере). Они делают полезные вещи, например, создают образцы текстуры и отслеживают время.

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

unity варит полезные инструменты шейдеров
UnityCG.cginc include в Unity содержит предопределенные переменные и вспомогательные функции.

Теперь взгляните на включенные структуры:

struct appdata
{
   float4 vertex : POSITION;
   float2 uv : TEXCOORD0;
};

struct v2f
{
   float2 uv : TEXCOORD0;
   UNITY_FOG_COORDS(1)
   float4 vertex : SV_POSITION;
};

На этот раз есть две входные структуры вместо одной входной структуры для поверхностного шейдера. Вы вводите первую структуру в вершинный шейдер, который выводит вторую структуру. Эти выходные данные вершинного шейдера затем поступают на вход фрагментного шейдера.

Чуть ниже sampler2D _MainTex; В строке основного кода Cg добавьте следующие новые свойства:

float4 _MainTex_ST;
half _Opacity;
float _AnimSpeedX;
float _AnimSpeedY;
float _AnimScale;
float _AnimTiling;

Как и в предыдущем Surface Shader, вы должны объявить все свойства как переменные в Cg-коде. Вы будете использовать эти свойства, чтобы изменить текстуру и анимировать ее в основной функции шейдера.

Теперь в основной функции фрагмента шейдера, прямо вверху (над комментарием кода // sample the texture добавьте:

// distort the UVs
i.uv.x += sin((i.uv.x + i.uv.y) * _AnimTiling + _Time.y * _AnimSpeedX) * _AnimScale;
i.uv.y += cos((i.uv.x - i.uv.y) * _AnimTiling + _Time.y * _AnimSpeedY) * _AnimScale;

Эти две строки кода — основа анимированного водного эффекта. Этот код смещает UV-координаты, которые он получает, причем величина смещения изменяется со временем.

В результате получается волнообразная анимация текстуры. Между тем, обе строки кода делают по сути одно и то же, но в разных направлениях. Вы поймете и то, и другое, если подробно разберете одну строку.

Сначала код вызывает функцию тригонометрии, синус или косинус, так что значение будет меняться на повторяющейся волне. _AnimTiling масштабирует переданное число. Как следует из названия, _AnimTiling управляет мозаикой эффекта — в данном случае, воздействуя на частоту синусоидальной волны.

Затем вы добавляете время к переданному числу, заставляя возвращаемое значение перемещаться по синусоиде с течением времени. Код библиотеки из более раннего включает переменную, которая отслеживает время.

_Time — это вектор из четырех чисел, при этом четыре значения представляют собой время, масштабированное для удобства в различной степени. Вот почему в коде используется _Time.y, а не просто _Time; y — второе число в векторе.

Наконец, вы умножаете все на _AnimScale, свойство, которое контролирует, насколько сильным будет эффект. Чем больше масштаб, тем больше волнистость.

Наконец, чуть выше return col; в строке кода функции frag добавьте:

col.a = _Opacity;

Здесь код просто устанавливает альфа-значение для свойства _Opacity. Обязательно сделайте это после выборки текстуры; в противном случае текстура перезапишет альфа-значение.

Теперь, когда вы написали свой шейдер, вы можете назначить его материалу cartoon-water.

По умолчанию поверхность воды не будет анимирована в режиме просмотра Scene, но будет отображаться в режиме просмотра Game, когда вы нажмете Play.

шейдер анимирует волны для волнообразного эффекта воды

Куда двигаться дальше?

Вы можете скачать готовые файлы проекта, нажав кнопку «Скачать материалы урока» вверху урока.

Теперь вы знаете основы написания собственных шейдеров в Unity! Конечно, есть еще много чего, что стоит изучить, и хороший ресурс для изучения — это руководство по шейдерам Unity.

Помните ту удобную функцию шейдера, которую вы использовали ранее? Что ж, если вам интересно, какие вспомогательные функции доступны с этим включением, вы можете проверить их здесь.

Если вам интересно узнать о шейдерах с помощью Unity, ознакомьтесь с нашим другим руководством по Shader Graph в Unity здесь.

Надеюсь, вам понравился этот урок! Если у вас есть какие-либо вопросы, комментарии или предложения, не стесняйтесь оставлять их в комментариях.

Автор перевода: Jean Winters

Источник: Introduction to Shaders in Unity

Комментировать

Почта не публикуется.Обязательные поля отмечены *