Привет, меня зовут Нед, и я делаю игры! Вы когда-нибудь задумывались, как работают шейдеры в Unity? Или вы хотите написать свои собственные шейдеры для Universal Render Pipeline, но без Shader Graph? Либо потому, что вам нужна какая-то особая функция, либо вы просто предпочитаете писать код, этот учебник поможет вам.

Это третье руководство из серии, посвященной шейдерам HLSL для URP. Если вы начинаете здесь, я бы порекомендовал хотя бы скачать стартовые скрипты здесь, чтобы вам было с чем работать.

  1. Введение в шейдеры: простые неосвещенные шейдеры с текстурами.
  2. Простое освещение и тени: направленный свет и отбрасываемые тени.
  3. Прозрачность: смешанная и вырезанная прозрачность.
  4. Физически обоснованный рендеринг: карты нормалей, металлические и зеркальные рабочие процессы, а также дополнительные режимы наложения.
  5. Расширенное освещение: точечные, точечные и запеченные источники света и тени.
  6. Расширенные функции URP: глубина, нормали глубины, окружающее затенение экранного пространства, однопроходный рендеринг VR, пакетная обработка и многое другое.
  7. Пользовательские модели освещения: доступ к данным об освещении и их использование для создания собственных алгоритмов освещения.
  8. Анимация вершин: анимация мешей в шейдере.
  9. Сбор данных из C#: дополнительные данные вершин, глобальные переменные и процедурные цвета.

Если вы предпочитаете видеоуроки, вот ссылка на видеоверсию этой статьи.

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

Прежде чем я двинусь дальше, я хочу поблагодарить всех своих покровителей за помощь в создании этой серии, и передать большой привет моему покровителю «следующего поколения»: Crubidoobidoo! Большое спасибо всем вам.

Не будем медлить и приступим к программированию!

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

Эта команда Смешать определяет, как растеризатор объединяет выходные данные функции фрагмента с цветами, уже присутствующими на экране (или в целевом объекте рендеринга). Они были нарисованы шейдерами, которые запускались раньше! Цвет, возвращаемый функцией фрагмента, называется исходным цветом, а цвет, хранящийся в целевом объекте рендеринга, называется целевым цветом.

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

Blend One Zero приводит к полностью непрозрачным материалам — значение по умолчанию. Для прозрачности нам нужно линейно интерполировать между исходным и конечным цветами на основе альфа-канала исходного цвета. К счастью, в ShaderLab есть множитель «исходный альфа» и «один минус исходный альфа», идеально подходящий для наших целей. Добавьте это к блоку прохода с подсветкой вперед.

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

Режимы ZWrite. Помните буфер глубины из нашего обсуждения отображения теней? В настоящее время растеризатор сохраняет позиции прозрачных объектов в буфере глубины, предотвращая запуск фрагментов позади него. Мы не можем смешивать цвета с материалом, который никогда не рисовали! Нам нужен способ предотвратить сохранение прозрачных поверхностей в буфере глубины.

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

Теперь поверхности за этим шейдером всегда будут отрисовываться.

Очереди рендеринга. Хм, но есть еще некоторые странности. Скайбокс полностью перезаписывает все прозрачные объекты — из-за порядка рендеринга.

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

При подготовке к рендерингу сцены URP просматривает все объекты, которые можно рендерить, и сортирует их по очереди рендеринга. Существует несколько очередей, в которые можно поместить шейдеры с помощью тега Queue, установленного в блоке тегов SubShader. Настройкой по умолчанию является «Геометрия», используемая для непрозрачных материалов. Тег очереди «Прозрачный» идет после геометрии. Поместив MyLit в эту очередь, мы можем обеспечить отрисовку непрозрачных объектов в первую очередь.

Существует также очередь Skybox, которая проходит между Geometry и Transparent. Раньше скайбокс визуализировался после MyLit, а поскольку в MyLit была функция ZWrite Off, растеризатор позволял шейдеру скайбокса перезаписывать его. При использовании прозрачной очереди это не проблема.

Для отладки и некоторых других более сложных систем в Unity также есть тег RenderType. Для этого следует установить значение «Непрозрачный» или «Прозрачный». RenderType не влияет на порядок рендеринга, но давайте настроим его прямо сейчас.

Теперь обе прозрачные сферы должны появиться перед скайбоксом. Обратите внимание, что сферы даже правильно рисуются друг над другом. Зная, что мы делаем с порядком рендеринга, вы можете догадаться — и правильно — что URP сортирует объекты в одной очереди по расстоянию от камеры, сзади вперед. Это очень важно для прозрачных шейдеров.

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

Прежде чем двигаться дальше, взгляните на отладчик кадров. Вы можете увидеть порядок рендеринга и убедиться, что каждый объект находится в правильной очереди. Посмотрите в разделе «DrawTransparentObjects!»

Если по какой-то причине вам нужно настроить порядок прорисовки на основе материала, у каждого материала есть поле очереди в нижней части его инспектора. Вы можете менять очереди и даже отдавать приоритет определенным материалам. Очередь «Geometry+1» запускается после всех остальных объектов в очереди геометрии, но все же перед очередью скайбокса.

Вы могли заметить еще одну ошибку: прозрачные объекты отбрасывают полностью непрозрачные тени. Прозрачные или частичные тени очень сложны, и я не буду рассматривать их в этой серии. Хорошо это или плохо, но шейдер Lit также не поддерживает прозрачные тени. Лучшее, что мы можем сделать, это отключить тени для прозрачных объектов. Но для этого нам нужно настроить некоторую инфраструктуру C#…

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

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

Скрипты редактора Unity помещаются в папки с именем «Редактор», поэтому сначала создайте их. Внутри создайте скрипт C# под названием «MyLitCustomInspector».

Откройте его и удалите функции «Пуск» и «Обновление». Измените MyLitCustomInspector на наследование от ShaderGUI, расположенного в пространстве имен UnityEditor.

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

Прежде чем идти дальше, настройте некоторые свойства в файле шейдера. В свойствах хранятся не только значения, используемые в коде HLSL, но и метаданные о материале. Создайте свойство Float _SurfaceType, чтобы помнить, является ли этот материал непрозрачным или прозрачным. Добавьте атрибут HideInInspector, чтобы это свойство не отображалось в инспекторе, ориентированном на пользователя.

Затем создайте три свойства Float для исходного смешивания, целевого смешивания и режимов z-записи. Чтобы указать ShaderLab использовать значение свойства в командах Blend и ZWrite, заключите имя в квадратные скобки. Сделайте это для прохода ForwardLit. Создатель тени может использовать значения по умолчанию для Blend One Zero и ZWrite On.

Теперь нам нужно изменить теги Queue и RenderType! К сожалению, чистый ShaderLab не поддерживает теги переменных — позаботьтесь об этом в пользовательском инспекторе C#! На данный момент сбросьте «RenderType» на «Opaque» и удалите тег «Queue» — по умолчанию он будет «Geometry».

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

Материал сохраняет текущее значение в своем свойстве «_SurfaceType»; однако не рекомендуется читать его прямо из материала. Для поддержки сериализации, отмены и других функций редактора Unity предоставляет класс MaterialProperty. Он передает список, содержащий экземпляры MaterialProperty для каждого свойства шейдера. Используйте метод FindProperty, чтобы выбрать один, соответствующий «_SurfaceType».

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

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

Создайте функцию UpdateSurfaceType, принимая материал в качестве аргумента. Вызовите его внутри блока EndChangeCheck if. UpdateSurfaceType обновит режим ZWrite, режимы Blend, теги и генератор теней на основе _SurfaceType. Поскольку функция запускается после ввода, безопасно читать свойства непосредственно из материала.

Используйте оператор switch, чтобы обновить материал на основе перечисления SurfaceType. Установите очередь рендеринга с помощью Material.renderQueue. Переопределите тег RenderType с помощью SetOverrideTag. Задайте свойства ZWrite и Blend с помощью SetInt. Есть удобное перечисление от Unity для режимов наложения — включите пространство имен Unity.Rendering для доступа к нему. Что касается ZWrite, 1 соответствует Вкл., а 0 — Выкл..

Наконец, чтобы отключить тени, мы можем отключить проход заклинателя теней. Для этого есть SetShaderPassEnabled.

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

Все, что осталось сделать, это зарегистрировать этот класс инспектора в нашем шейдере. В файле .shader используйте команду CustomEditor внутри блока Shader.

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

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

Класс ShaderGUI имеет обратный вызов при назначении нового шейдера материалу: AssignNewShaderToMaterial. Переопределите его и оставьте базовый вызов наверху. Проверьте, является ли новый шейдер шейдером «MyLit», используя поле его имени. Если true, вызовите UpdateSurfaceType.

Еще один баг исправлен!

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

Это гибридный режим! Каждый фрагмент либо полностью непрозрачен, либо полностью прозрачен. Смешивание не требуется, что позволяет безопасно записывать в буфер глубины и отбрасывать тени.

Для поддержки вырезов требуется небольшая корректировка почти всего кода, который мы написали до сих пор, но давайте начнем с пользовательского инспектора. Добавьте элемент TransparentCutout в перечисление SurfaceType. Пока мы здесь, переименуйте режим Transparent в TransparentBlend, чтобы было понятнее.

В UpdateSurfaceType нам нужно настроить все для вырезов. У них те же режимы Blend и ZWrite, что и у непрозрачных поверхностей, но другая очередь и тег типа рендеринга. Они также отбрасывают тени. Реорганизуйте оператор switch, чтобы учесть все это.

Очередь AlphaTest. Примечательно, что вырезы должны запускаться в новой очереди под названием «AlphaTest». Эта очередь запускается после геометрии, но перед скайбоксом. Но, поскольку вырезы не нуждаются в смешивании, почему бы просто не использовать геометрию? Это вопрос оптимизации.

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

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

Вырезать и удалить. Но вернемся к коду. В MyLitForwardLitPass.hlsl используйте эту функцию clip HLSL, чтобы выполнить работу. Если вы передадите ему число, меньшее или равное нулю, он отбросит текущий запущенный фрагмент. Что это значит?

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

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

Попробуйте! Возьмите текстуру с альфа-каналом и измените свой материал на режим «Прозрачный вырез». Довольно круто, но есть несколько вещей, которые нужно исправить.

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

Определите свойство «_Cutoff» в файле .shader. Он всегда должен находиться в диапазоне от нуля до единицы, поэтому используйте специальный тип свойства Range для создания ползунка.

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

Затем в MyLitForwardLitPass.hlsl определите _Cutoff рядом с другими свойствами и вычтите _Cutoff из альфы вместо 0,5.

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

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

Для этого есть ярлык: #ifdef. Имейте в виду, что по какой-то причине не существует #elifdef, поэтому я иногда использую весь определенный синтаксис для ясности. В любом случае используйте ключевое слово _ALPHA_CUTOUT, чтобы включить или опустить функцию клипа. Оберните вызов clip в блок #ifdef, и все готово.

Ключевые слова из C# можно включать и отключать, поэтому позаботьтесь об этом в пользовательском инспекторе. В UpdateSurfaceType включите или отключите _ALPHA_CUTOUT в зависимости от типа поверхности.

Теперь нам нужны варианты шейдера на основе _ALPHA_CUTOUT. Перейдя к файлу .shader, добавьте #прагму для их создания. Вместо multi_compile используйте команду shader_feature_local. Функции шейдеров аналогичны множественным компиляциям в том смысле, что они генерируют варианты шейдеров на основе списка ключевых слов. Разница в билдах игры.

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

Unity включает все варианты, сгенерированные командами multi_compile, но перед включением варианта shader_feature проверяет, включены ли для некоторых материалов требуемые ключевые слова. Например, Unity включает варианты MyLit только с включенным _ALPHA_CUTOUT, если материал имеет тип поверхности выреза (и, таким образом, включает ключевое слово _ALPHA_CUTOUT).

Поскольку эта проверка выполняется во время сборки, ключевые слова, которые динамически изменяются во время выполнения (например, ключевые слова освещения URP), должны использовать multi_compile. В противном случае используйте shader_feature.

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

Функции шейдеров всегда имеют неявный «_» в списке ключевых слов; другими словами, они всегда запускают вариант, в котором не включено ни одно из перечисленных ключевых слов. Конечно, этот вариант может использоваться в вашей игре, а может и не использоваться, но Unity готова к такой возможности!

Наконец, суффикс local указывает, что ключевое слово уникально для этого шейдера и не будет устанавливаться глобально. Мы устанавливаем _ALPHA_CUTOUT для каждого материала отдельно, чтобы можно было использовать местные варианты. Ключевые слова, заданные URP глобально, например _MAIN_LIGHT_SHADOWS, не могут быть локальными. Unity имеет жесткое ограничение на количество поддерживаемых глобальных ключевых слов, поэтому рекомендуется использовать локальные варианты, когда это возможно.

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

Общие файлы HLSL. Следующая ошибка, которую нужно устранить, — это тени: объекты больше не отбрасывают тени, соответствующие форме их выреза! Чтобы исправить это, обрежьте фрагменты в проходе заклинателя теней.

Во-первых, давайте возьмем логику альфа-отсечения в MyLitForwardLitPass.hlsl и переместим ее в функцию с именем TestAlphaClip. Передайте образец цветовой текстуры в качестве аргумента. Чтобы сделать это доступным в MyLitShadowCasterPass.hlsl, мы должны добавить его в отдельный файл и включить #include в оба файла.

Создайте новый файл «MyLitCommon.hlsl». Удалите TestAlphaClip из MyLitForwardLitPass.hlsl и вставьте его сюда. TestAlphaClip требует нескольких свойств, _Tint и _Cutoff. Свойства определяются для всего шейдера, поэтому имеет смысл перенести определения свойств в общий файл. Включите библиотеку URP, чтобы иметь доступ к макросам TEXTURE2D.

В MyLitForwardPass.hlsl обрежьте повторяющийся код и добавьте новый файл общего кода с помощью #include.

Давайте на минутку задумаемся об этом. HLSL очень похож на C++, и #include заставляет компилятор буквально копировать и вставлять содержимое общего файла поверх строки #include. Что произойдет, если я дважды случайно #include файл? Unity выдаст ошибку о том, что переменная уже объявлена.

Хорошо, но дублирование строк #include встречается нечасто. Что, если MyLitCommon и MyLitForwardPass также #include файл с именем MyMath.hlsl? Тогда MyMath будет продублирован!

Для упрощения и предотвращения ошибок принято заключать весь код в файле HLSL внутрь чего-то, что называется защитным блоком ключевых слов. Во-первых, проверьте, не определено ли ключевое слово (это то, что делает #ifndef, сокращение для #if !defined()). В следующей строке #define защитное ключевое слово. Не забудьте #endif в конце файла!

Теперь при первом включении MyMath компилятор определяет ключевое слово guard. Во второй раз ключевое слово guard включено, и оно пропускает код внутри. Довольно умный! Идите вперед и добавьте ключевые слова защиты в MyLitCommon. Я также добавляю их во все файлы HLSL, чтобы быть в безопасности.

Клип в Shadow Caster. После всей этой очистки ваш шейдер должен по-прежнему работать так же. Давайте, наконец, добавим отсечение тени.

Во-первых, в файле .shader добавьте функцию шейдера _ALPHA_CUTOUT в проход отбрасывателя теней.

В MyLitShadowCasterPass.hlsl добавьте #include «MyLitCommon.hlsl». На данный момент создатель теней не имеет UV-развертки для сэмплирования основной текстуры. Нам нужно будет пройти их весь путь до стадии фрагмента.

Для этого добавьте поле uv в Interpolators. Каждое поле здесь — это другое поле, которое растеризатор должен интерполировать, и лучше всего сделать эту структуру как можно меньше. Окружите поле uv блоком #if, чтобы он интерполировался только при необходимости. Это не так важно делать в структуре Attributes, но это не помешает.

В вершинной функции передайте UV в выходную структуру, опять же, только если определен _ALPHA_CUTOUT.

Затем в функции фрагмента выберите цветовую текстуру и вызовите TestAlphaClip. Мы также можем обернуть все это в блок #if. Помните, что clip отбрасывает фрагменты, если передается значение ниже нуля, в результате чего растеризатор отбрасывает их и не записывает в буфер глубины. Так как буфер глубины становится картой теней, обрезанные фрагменты там тоже не будут отображаться.

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

Осталась еще одна проблема, которую я хочу решить, но она требует дополнительных объяснений…

Двусторонняя визуализация. Часто в играх используется альфа-отсечение на плоских плоскостях, и в этом случае все выглядит нормально. Но если вы включите альфа-отсечение на сфере, вы можете заметить, что внутренняя часть становится невидимой!

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

В нашем случае мы хотели бы визуализировать внутреннюю часть сферы. К счастью, отбраковку легко отключить.

В MyLit.shader добавьте новое свойство float с именем _Cull. Мы будем использовать его для установки другой команды ShaderLab, Cull. Отбраковка может принимать три различных значения: Выкл., Спереди и Сзади. Выкл. полностью отключает отбраковку, в то время как два других отбрасывают одну сторону треугольника.

В Unity есть enum в C# для этих параметров, и мы можем дать указание инспектору материалов по умолчанию создать раскрывающийся список, используя его. В enum Back имеет значение int, равное двум, поэтому давайте установим его по умолчанию.

Затем добавьте команду Cull к проходам прямого света и тени, указав свойство _Cull в скобках.

Вернитесь на сцену и попробуйте. Отключение отбраковки показывает обе стороны сферы!

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

В файле MyLitForwardLitPass.hlsl инвертируйте вектор нормали, указанный в InputData, при рендеринге обратной стороны треугольника. Чтобы перевернуть вектор, просто умножьте его на минус. Но как узнать, какая сторона треугольника рендерится? У растеризатора есть эта информация, и он делает ее доступной через специальную семантику, предназначенную только для этапа фрагмента.

Чтобы получить его, просто добавьте еще один аргумент в функцию Fragment. И точная семантика, и тип аргумента зависят от текущей платформы. К счастью, Unity предоставляет макросы, позволяющие обойти эту проблему. Несмотря на это, тип frontFace по сути является логическим значением, истинным, если передняя грань треугольника видна.

Unity также предоставляет другой макрос для выбора значения на основе frontFace, подобно тернарному оператору в C#. Используйте это, чтобы умножить вектор нормали на 1 или -1, прежде чем устанавливать его в lightingInput.

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

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

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

Давайте реализуем это в MyLitShadowCasterPass.hlsl, используя новую функцию FlipNormalBasedOnViewDir. Передайте ему положение и нормальное. Вычислите направление взгляда, используя встроенную функцию URP (которую мы также использовали в функции фрагмента с передним освещением). Затем, только если скалярное произведение нормали и направления взгляда меньше нуля, умножьте нормаль на отрицательную единицу. Верните норму.

Измените расчет пространства клипа, чтобы он вызывал FlipNormalBasedOnViewDir.

Вернувшись в редактор сцен, похоже, что теневых прыщей больше нет! Миссия выполнена!

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

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

Давайте решим эти проблемы, обновив пользовательский инспектор. Создайте новое enum, FaceRenderingMode, чтобы инкапсулировать вышеупомянутые режимы. Как и в поверхностном режиме, используйте другое скрытое свойство, чтобы отслеживать этот параметр. В OnGUI добавьте еще одно раскрывающееся меню enum, управляющее этим новым свойством: _FaceRenderingMode.

В UpdateSurfaceType обновите свойство _Cull и включите или отключите ключевое слово _DOUBLE_SIDED_NORMALS. Если режим рендеринга лица FrontOnly, установите для _Cull значение Back, используя CullMode enum Unity. >. В противном случае отключите отбраковку. Затем включите или отключите наше ключевое слово соответствующим образом.

Переходя к MyLit.shader, сначала скройте свойство _Cull в инспекторе и удалите атрибут Enum (сейчас он обрабатывается кодом). Во-вторых, добавьте еще одно скрытое свойство для _FaceRenderingMode. В-третьих, добавьте функцию шейдера для _DOUBLE_SIDED_NORMALS в оба прохода.

В MyLitForwardLitPass оберните весь обычный код переключения в блоки #if. Есть небольшая хитрость, чтобы скрыть семантику лицевой стороны, когда она не нужна. Это некрасиво, но это работает!

В MyLitShadowCasterPass аналогичным образом поместите вызов FlipNormalBasedOnViewDir в блок #if.

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

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

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

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

Для справки: вот окончательные версии файлов шейдеров.

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

Если вы хотите увидеть этот урок под другим углом, я создал видеоверсию, которую вы можете посмотреть здесь.

Я хочу поблагодарить Crubidoobidoo за всю их поддержку, а также всех моих покровителей во время разработки этого руководства: Adam R. Vierra, Amin, Autumnboy, Ben Luker, Ben Wander, bgbg, Bohemian Grape, Боскайоло, Брэннон Нортингтон, Брук Уоддингтон, Кэмерон Хорст, Чарли Цзяо, Кристофер Эллис, CongDT7, Коннор Вендт, Крубидубиду, Дэн Пирс, Дэниел Сим, Давиде, Дерек Арндт, Донгсик Ган, Эльмар Мельцер, Эрен Айдин, немногие гиганты, Генри Чанг , Говард Дэй, Изобель Шаша, Джек Фелпс, Джон Лисм Фишман, Джон Луна, Джозеф Херст, Джей Пи Ли, jpzz Ким, Джей Я, Кэт, Кайл Харрисон, Лассерено, Ливензо (Башня уединения), Лекси Досталь, Лхонг Ли, Лиен Дин, Лукас Шнайдер, Безумная наука, Марчин Кшешовец, Маттаи, Минь Триет О, Оливер Дэвис, PW, Патрик, Патрик Бергстен, Рафаэль Людешер, Ричард Питерс, Робин Бензингер, Сэм CD-ROM, Сэмюэл Анг, Сандро Траеттино, Сантош, SHELL SHELL, Саймон Джексон, starbi, Steph, Stephan Maier, Steve DeBusschere, Syll art-design, Taavi Varm, Team 21 Studio, thearperson, Thomas Terkildsen, Tim Hart, Tomasz Patek, ultraklei, Vincent Thémereau, Voids Adrift, Wei Suo, Wojciech Marek, Ксавьер Ларроса Рогель

Если вы хотите загрузить все шейдеры, представленные в этом руководстве, внутри проекта Unity, подумайте о том, чтобы присоединиться к моему Patreon. Вы также получите ранний доступ к учебным пособиям, право голоса в тематических опросах и многое другое. Спасибо!

Если у вас есть какие-либо вопросы, не стесняйтесь оставлять комментарии или связаться со мной по любой из моих ссылок в социальных сетях:

🔗 Список обучающих сайтов ▶️ YouTube 🔴 Twitch 🐦 Twitter 🎮 Discord 📸 Instagram 👽 Reddit 🎶 TikTok 👑 PatreonKo-fi 📧 E-mail: nedmakesgames Gmail

Большое спасибо за чтение и создание игр!

©️ Тимоти Нед Аттон 2022. Все права защищены.

Весь код, появляющийся в GitHub Gists, распространяется под лицензией MIT.

Тимоти Нед Аттон — разработчик игр и инженер-график с десятилетним опытом работы с Unity. В настоящее время он работает в компании Golf+, работая над VR-игрой в гольф Golf+. Это руководство не связано и не одобрено Golf+, Unity Technologies или какими-либо людьми и организациями, перечисленными выше. Спасибо за прочтение!