Масштабируемая и высокая производительность рендеринга
Universal Render Pipeline от Unity стал мощным решением, сочетающим красоту, скорость и производительность, а также поддержку всех целевых платформ Unity.
Universal Render Pipeline — это:
В Universal Render Pipeline постобработка встроена непосредственно в процесс рендеринга, обеспечивая высокую производительность. Разработчикам доступны такие эффекты, как сглаживание, глубина резкости, размытие в движении, проекция Панини, блеск, искажение линзы, хроматические аберрации, цвето- и тонокоррекция, виньетка, зернистость и 8-битный дизеринг.
Universal Render Pipeline
Упор на производительность
Однопроходный упреждающий рендеринг
Поддержка Shader Graph
Встроенный процесс рендеринга
Универсальность
Поддерживает как упреждающий, так и отложенный рендеринг
От 2D и 3D до AR/VR-проектов: Universal Render Pipeline позволяет не тратить время на доработку проекта для выпуска на новом устройстве.
Возможность конфигурации рендеринга в Unity с помощью скриптов на C# позволяет вам:
Universal Render Pipeline проще, чем встроенный процесс рендеринга, но улучшает качество графики. Перевод проекта со встроенного процесса рендеринга на Universal Render Pipeline должен обеспечивать аналогичную или улучшенную производительность. Прочтите статью в нашем блоге, чтобы узнать о том, как Universal Render Pipeline повышает частоту кадров без снижения качества графики.
Вы можете воспользоваться преимуществами готовых технологий уже сегодня. Обновите проекты с помощью средств перехода или создайте новый проект на основе нашего шаблона Universal через Unity Hub.
Разработка мобильных игр на Unity. URP, 2D Animation и другие новомодные вещи на примере игры
Дисклеймер! Код в этой статье не проходил рефакторинг и носит лишь ознакомительный характер, чтобы поделиться идеями. И вообще, в целом, это smellscode.
Итак, запасаемся кофе, открываем Unity и погнали!
Базовая настройка проекта. URP и все-все-все.
Стоит указать, что ниже пойдет речь о 2D игре. Для 3D игр подходы будут несколько отличаться, как и настройки.
В нашем проекте стоят следующие настройки (для Quality уровней):
Настройки графики для пресета Low в Project Settings:
На что здесь следует обратить внимание:
Теперь перейдем к настройкам самих URP Asset. На что следует обратить внимание:
Adaptive Performance
Отличная штука для автоматической подгонки производительности мобильных игр (в частности для Samsung-устройств):
Другие полезные настройки:
Отключите 3D освещение, лайтмапы, тени и все что с этим связано.
По-возможности подключите multithreaded rendering.
Игровой фреймворк
Едем дальше. URP и другие настройки проекта сделали. Теперь настало время поговорить о нашем ядре проекта. Что оно включает в себя?
Само ядро фреймворка включает в себя:
Игровые менеджеры для управления состояниями игры, аудио, переводов, работы с сетью, аналитикой, рекламными интеграциями и прочим.
Базовые классы для интерфейсов (компоненты, базовые классы View).
Классы для работы с контентом, сетью, шифрованием и др.
Базовые классы для работы с логикой игры.
Базовые классы для персонажей и пр.
Утилитарные классы (Coroutine Provider, Unix Timestamp, Timed Event и пр.)
Зачем нужны менеджеры?
Они нужны нам для того, чтобы из контроллеров управлять состояниями и глобальными функциями (к примеру, аналитикой).
Хотя мы и используем внедрение зависимостей, менеджеры состояний реализованы в качестве синглтонов (атата по рукам, но нам норм) и могут быть (и по их назначению должны быть) инициализированы единожды. А дальше мы просто можем использовать их:
А уже сам менеджер распределяет, в какие системы аналитики, как и зачем мы отправляем эвент.
Базовые классы.
Здесь все просто. Они включают в себя базовую логику для наследования. К примеру, класс BaseView и его интерфейс:
А дальше мы можем использовать его, к примеру таким образом:
Классы для работы с контентом, сетью, шифрованием
Ну здесь все просто и очевидно. Вообще, у нас реализовано несколько классов:
1) Классы шифрования (Base64, MD5, AES и пр.)
3) Network-классы, которые позволяют удобно работать с HTTP-запросами, работать с бандлами / адрессаблс и др.
Классы для шифрования нужны, чтобы работать с сохранениями и передачей данных на сервер в безопасном формате (относительно безопасном, но от школьников уже спасет).
Утилитарные классы
Здесь у нас хранятся полезные штуки, вроде Unix Time конвертера, а также костыли (вроде Coroutine Provider-а).
Unix Time Converter:
Костыль Coroutine-Provider:
Логика сцен
Зачем это сделано?
Мы можем сохранять прогресс внутри сцены, привязываясь к определенному блоку.
Блоки механик удобнее изменять, нежели огромный инсталлер с кучей разных контроллеров.
Работа с контентом
При работе с контентом, мы стараемся делать упор на оптимизацию. В игре содержится много UI, скелетные 2D анимации, липсинк и прочее. Вообще, контента достаточно много, не смотря на простоту игры.
Анимации в игре
Упаковка и сжатие
Локализация
Вся локализация базируется на JSON. Мы планируем отказаться от этого в ближайшее время, но пока что на время Soft-Launch этого хватает:
Работа с UI
При работе с UI мы разбиваем каждый View под отдельный Canvas. 99% всех анимаций работает на проверенном временем DOTween и отлично оптимизирован.
View инициализируются и обновляются по запросу через эвенты, которые внедряются в Level Installer, либо в отдельных блоках логики.
Что мы используем еще?
Итого
Работа с механиками получается достаточно гибкой за счет блоков логики. Мы изначально думали взять связку Zenject + UniRX, но решили отказаться от нагромождения большой системы. Да, мы сделали проще, но нам и не нужно всех возможностей этих огромных библиотек.
Пишем шейдеры кодом в Unity URP (LWRP)
Введение
Здравствуй, Хабр. Сегодня хочется рассказать немного о том, как можно быстро и безболезненно (почти) начать писать классические текстовые шейдеры в Unity с использованием Lightweight Rendering Pipeline (LWRP, а ныне URP — Universal Render Pipline) — одним из примеров конвейера Scriptable Rendering Pipeline (SRP).
А как же Shader Graph?
Shader Graph — это удобное и быстрое средство прототипирования или написания простых эффектов. Однако, порою, требуется написать нечто сложное и комплексное и вот тогда — количество нод, кастомных функций, суб-графов неимоверно увеличивается, отчего даже самый матёрый программист графики начинает путаться во всём этом бардаке. Все мы понимаем, что автоматически генерируемый код априори не может быть лучше написанного вручную — за примерами ходить далеко не нужно, ибо любая ошибка в планировке нод может привести к тому, что уже известный результат вычислений в вершинном шейдере будет посчитан повторно во фрагментом. Бывают и люди, которым просто удобнее работать с кодом, а не с нодами. Причины могут быть разными, но суть одна — долой ноды, да здравствует код!
Проблематика
Итак, в чём же проблема сесть и написать обычный текстовый шейдер под LWRP? А проблема в том, что всеми любимые Standard Surface Shader-ы не поддерживаются в LWRP.
При попытке его использования мы получаем следующие:
Тогда на ум приходит попробовать написать обычный анлит шейдер с вертексной и фрагментной частью. И к нашему счастью, всё работает:
Однако как тут можно не грустить — мы словно остались голыми на обочине без света, теней, лайтмап и любимого PBR, без которого и жизнь не мила.
Конечно, можно написать всё руками:
Да будет свет!
Вроде бы всё работает, но этого всего лишь дифузное освещение. Что же делать дальше? Можно и дальше возвращать всё руками, но это долго и муторно, да и PBR никак не получится вернуть и всех фишек LWRP мы лишаемся. Поэтому нам ничего не остаётся как ковырять LWRP, чтобы одним волшебным взмахом всё вернуть.
Решение
Собственно, придя по указаному адресу и зайдя в папку Shaders мы можем обнаружить один интересный шейдер, с названием Lit.shader. Собственно, можно сказать наши поиски окончены, вот он — заветный шейдер. Зайдя внутрь — мы обнаружим следующие содержание:
Остаётся только развернуть его для удобства редактирования, избавившись от include-ов. Ну и немного модифицировать на свой лад.
Получаем нечто подобное:
Прежде всего, нужно разобрать что к чему в получившемся шейдере. Во-первых, нас интересует строчка:
Которая, как мы помним, в оригинале была следующей:
Итак, что же это и зачем это? Ответ на этот вопрос достаточно прост — если присмотрется, в шейдере огромное количество дефайнов разных сортов, которые, как ни странно, нужно активировать и деактивировать, а это, на минуточку, нужно делать из кода, именно поэтому нам потребуется кастомный инспектор. Более того, наш кастомный инспектор должен давать нам возможность редактировать не только встроенные проперти, но и те, которые нам могут понадобиться в своих шейдерах. У исходного шейдера уже имеется кастомный инспектор, поэтому уверенно топаем его искать по следующему пути:
Собственно, нас интересует файл LitShader.cs, который унаследован от BaseShaderGUI:
Основной замес происходит как раз таки в BaseShaderGUI.cs, который можно найти в папке на уровень выше:
Берём эти скрипты и кидаем в проект в папку Editor (если такой нет — создать, иначе во время билда проекта закономерно появятся ошибки, так как пространство имён UnityEditor не входит в сборку билда). Разумеется, на нашу голову валится тысяча и одна ошибка, которые связаны с интернел-типом SavedBool, который представляет из себя сериализуемую в окне редактора переменную типа bool. Сделано это для сохранения состояния свёрнутости разделов материала. Собственно, для исправления проводим нехитрую манипуляцию.
И добавим ещё одну переменную для дополнительных кастомных пропертисов:
Также нужно добавить название и описание раздела с нашими кастомными параметрами, соблюдая сложившиеся традиции внутри скрипта:
Ну а теперь провернём небольшой фокус. Как можно заметить, у всех стандартных пропертисов я выставил атрибут HideInInspector, который своим названием напрямую намекает на то, что данный проперти будет скрыт в инспекторе. Однако, это релевантно только для стандратного инспектора материалов, а у нас какой? Правильно, кастомный! А это значит, что все наши встроенные пропертисы отрисуются по-любому. Вот и скроем их:
А внутри кода кастомного редактора просто вызовем отрисовку стандартного инспектора:
А вот и код обоих скриптов:
Теперь посмотрим внутренности шейдера.
Первое на что обращаешь внимание — это то, что в шейдере всего пять пассов. Немного остановимся на них:
Поэтому мы принудительно подключаем его:
А вот DirectX 9 не поддерживается, поэтому принудительно отключаем его:
В плане написания самого кода — ничего не изменилось за исключением того, что теперь мы пишем не на CG, а на чистом HLSL, а следовательно теперь тело шейдерной программы будет выглядеть следующем образом:
Собственно, пройдя по всё тому же магическому пути и найдя файл Core.hlsl:
Мы можем обнаружить полный список доступных функций.
Стоит также упомянуть константные буферы (CBUFFER) и UnityPerMaterial. Константные буферы используются для хранения данных, которые редко изменяются на GPU, соотвественно их можно использовать для хранения переменных шейдера. Для этого достаточно вызывать макросы
CBUFFER_START и CBUFFER_END:
Объявление глобальных переменных или различных параметрически-задаваемых (из кода или анимации, например) происходит по-старинке внутри тела шейдерной программы.
LWRP использует два типа константных буферов — UnityPerObject и UnityPerMaterial. Эти буфера биндятся один раз, чтобы их можно было использовать во время отрисовки. Грубо говоря, это означает, что во время отрисовки константные буферы не будут ребиндится или не будет вызываться setpass для материалов. Это выгодно, когда несколько шейдеров совместно используют один и тот же константный буфер, так как для этого LWRP может пакетировать различные материалы.
Собственно, если внимательно изучить структуру шейдера — можно обнаружить, что большая часть стандартных данных как раз и повсеместно использует константные буфера.
Более подробно, обо всех различиях, но на английском, можно почитать вот тут.
Кстати, если внимательно присмотреться к SurfaceData:
То можно обнаружить, что это-то и есть заветный PBR Master из ShaderGraph.
Пример
Итак, теперь у нас полностью развязаны руки, а это значит что пришло время
устроить вакханалию! Добавим-ка для примера Vertex Displacement и Dissolve Effect, а все остальное пустим в пляс.
Очень удобно, что все пассы у нас перед глазами и мы можем редактировать всё комплексно.
Которые несомненно отобразятся в нашей собственной и горячо любимой вкладочке в инспекторе:
Сначала отправим геометрию в пьяный угар:
Ну а теперь время Dissolve и движения:
Также можно завезти Dissolve и в тень, тогда лёгким движением руки у нас появятся правильная тень, коей добиться в Shader Graph достаточно сложно, а тут — всего лишь пара строчек кода.
Ну и финальный код шейдера:
Заключение
Что же, вот и пришло время подвести итоги. Как стало понятно, в LWRP можно и даже нужно писать шейдеры кодом, ибо это сильно развязывает руки, помогая без всяких костылей писать крутые штуки, к примеру, свою систему освещения. Конечно, это не идёт ни в какое сравнение с удобным и привычном Standard Surface Shader-ом, однако может быть когда-нибудь у меня дойдут руки написать такой же удобный аналог для LWRP и HDRP, но об этом как-нибудь в другой раз.
Создание Outline эффекта в Unity Universal Render Pipeline
В Universal Render Pipeline, создавая свои RendererFeature, можно легко расширить возможности отрисовки. Добавление новых проходов в конвеер рендеринга позволяет создавать различные эффекты. В этой статье, используя ScriptableRendererFeature и ScriptableRenderPass, создадим эффект обводки объекта (Outline) и рассмотрим некоторые особенности его реализации.
Вступление или пару слов о Render Pipeline
Scriptable Render Pipeline позволяет управлять отрисовкой графики посредством скриптов на C# и контролировать порядок обработки объектов, света, теней и прочего. Universal Render Pipeline — это готовый Scriptable Render Pipeline, разработанный Unity и предназначенный на замену старому встроенному RP.
Возможности Universal RP можно расширить, создавая и добавляя свои проходы отрисовки ( ScriptableRendererFeature и ScriptableRenderPass ). Об этом и будет текущая статья. Она пригодится тем, кто собирается переходить на Universal RP и, возможно, поможет лучше понимать работу имеющихся ScriptableRenderPass’ов в Universal RP.
При написании этой статьи использовалась Unity 2019.3 и Universal RP 7.1.8.
План действий
Мы будем разбираться в работе ScriptableRendererFeature и ScriptableRenderPass на примере создания эффекта обводки непрозрачных объектов.
Для этого создадим ScriptableRendererFeature, выполняющую следующие действия:
И последовательность результатов, которые мы должны достичь:
В ходе работы мы создадим шейдер, в глобальные свойства которого будут сохраняться результаты первого и второго проходов. Последний проход отобразит результат работы самого шейдера на экран.
Это свойства, объявленные в шейдере, но не имеющие определения в блоке Properties. В данном примере нет принципиальной разницы как мы будем задавать текстуры — через глобальные или обычные свойства.
Основная причина использования глобальных свойств — нет необходимости передавать материал каждому проходу. А также отладка становится немного удобнее.
Создаём OutlineFeature
ScriptableRendererFeature используется для добавления своих проходов отрисовки (ScriptableRenderPass) в Universal RP. Создадим класс OutlineFeature, наследуемый от ScriptableRenderFeature и реализуем его методы.
Метод Create() служит для создания и настройки проходов. А метод AddRenderPasses() для внедрения созданных проходов в очередь отрисовки.
ScriptableRenderer — текущая стратегия рендеринга по умолчанию для Universal RP. На данный момент в Universal RP реализована только стратегия Forward Rendering.
RenderingData содержит данные для настройки проходов отрисовки — данные об отбраковке, камерах, освещении и прочем.
Теперь приступим к созданию проходов отрисовки, а к текущему классу будем возвращаться после реализации каждого из них.
Render Objects Pass
Задача этого прохода — отрисовывать объекты из определенного слоя с заменой материала в глобальное свойство-текстуру шейдера. Это будет упрощенная версия имеющегося в Universal RP прохода RenderObjectsPass, с единственным отличием в цели ( RenderTarget ), куда будет производится отрисовка.
Создадим класс MyRenderObjectsPass, наследуемый от ScriptableRenderPass. Реализуем метод Execute(), который будет содержать всю логику работы прохода, а так же переопределим метод Configure().
Метод Configure() используется для указания цели рендеринга и создания временных текстур. По умолчанию целью является цель текущей камеры и после выполнения прохода она вновь будет указана по умолчанию. Вызов этого метода осуществляется перед выполнение основой логики прохода.
Замена цели рендеринга
Объявим RenderTargetHandle для новой цели рендеринга. Используя его, создадим временную текстуру и укажем её как цель. RenderTargetHandle содержит в себе идентификатор используемой временной RenderTexture. А также позволяет получить RenderTargetIdentifier, служащий для идентификации цели рендеринга, которая может быть задана, например как объект RenderTexture, Texture, временная RenderTexture или встроенная (используется камерой при отрисовке кадра).
Объект RenderTargetHandle будет создаваться в OutlineFeature и передаваться нашему проходу при его создании.
Метод GetTemporaryRT() создаёт временную RenderTexture с заданными параметрами и устанавливает её как глобальное свойство шейдера с указанным именем (имя будет задаваться в фиче).
Для создания временной RenderTexture используем дескриптор текущей камеры, содержащий информацию о размере, формате и прочих параметрах цели камеры.
Указание цели и её очистка должны происходить только в Configure() с использованием методов ConfigureTarget() и ClearTarget().
Рендер
Подробно рассматривать отрисовку не будем, т.к. это может увести нас далеко и надолго от основной темы. Для отрисовки воспользуемся методом ScriptableRenderContext.DrawRenderers(). Создадим настройки для отрисовки только непрозрачных объектов только из указанных слоёв. Маску слоя будем передавать в конструктор.
Замена материала
Переопределим используемые материалы при отрисовке, так как нам нужны только контуры объектов.
Шейдер для отрисовки
Создадим в ShaderGraph шейдер материала, который будет использоваться при отрисовке объектов в текущем проходе.
Добавляем проход в OutlineFeature
Вернемся в OutlieFeature. Для начала создадим класс для настроек нашего прохода.
Объявим поля для настроек MyRenderPass и имени глобального свойства-текстуры, используемой в качестве цели рендеринга нашим проходом.
Создадим идентификатор для свойства-текстуры и экземпляр MyRenderPass.
В методе AddRendererPass добавляем наш проход в очередь на исполнение.
Результат прохода для исходной сцены должен получиться следующий:
Blur Pass
Цель этого прохода — размыть изображение, полученное на предыдущем шаге и установить его в глобальное свойство шейдера.
Для этого несколько раз будем копировать исходную текстуру во временные, с применением к ней шейдера размытия. При этом исходное изображение можно уменьшить в размерах (создать уменьшенную копию), что ускорит расчеты и не повлияет на качестве результата.
Создадим класс BlurPass, наследуемый от ScriptableRenderPass.
Заведём переменные под исходную, целевую и временные текстуры (и их ID).
Все ID для RenderTexture задаются через Shader.PropertyID(). Это не означает что где-то обязательно должны существовать такие свойства шейдера.
Добавим поля и под остальные параметры, которые сразу же инициализируем в конструкторе.
_blurMaterial — материал с шейдером размытия.
_downSample — коэффициент для уменьшения размера текстуры
_passesCount — количество проходов размытия, которое будет применено.
Для создания временных текстур создадим дескриптор со всей необходимой информацией о ней — размере, формате и прочем. Высоту и размер будем масштабировать относительно дескриптора камеры.
Также создадим идентификаторы и сами временные RenderTexture.
Мы снова меняем цель рендеринга, поэтому создадим ещё одну временную текстуру и укажем её как цель.
Размытие
Некоторые задачи рендеринга могут быть выполнены с помощью специальных методов ScriptableRenderContext, которые настраивают и добавляют в него команды. Для выполнения других команд потребуется использовать CommandBuffer, который можно получить из пула.
После добавления команд и отправки их на выполнение контексту, буфер нужно будет вернуть обратно в пул.
Конечная реализация метода Execute() будет следующей.
Шейдер
Для размытия создадим простой шейдер, который будет вычислять цвет пикселя с учётом его ближайших соседей ( среднее значение цвета пяти пикселей ).
Shader «Custom/Blur»
<
Properties
<
_MainTex («Texture», 2D) = «white» <>
>
struct Attributes
<
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
>;
struct Varyings
<
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
>;
TEXTURE2D_X(_MainTex);
SAMPLER(sampler_MainTex);
float4 _MainTex_TexelSize;
Varyings Vert(Attributes input)
<
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = input.uv;
return output;
>
half4 Frag(Varyings input) : SV_Target
<
float2 offset = _MainTex_TexelSize.xy;
float2 uv = UnityStereoTransformScreenSpaceTex(input.uv);
half4 color = SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv);
color += SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv + float2(-1, 1) * offset);
color += SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv + float2( 1, 1) * offset);
color += SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv + float2( 1,-1) * offset);
color += SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv + float2(-1,-1) * offset);
#pragma vertex Vert
#pragma fragment Frag
Добавляем проход в OutlineFeature
Порядок действия будет аналогичен добавлению нашего первого прохода. Сначала создадим настройки.
И добавим в очередь на выполнение.
Outline Pass
Конечное изображение с обводкой объектов будет получено с помощью шейдера. И результат его работы будет отображаться поверх текущего изображения на экране.
Ниже представлен сразу весь код прохода, т.к. вся логика заключена в двух строках.
RenderingUtils.fullscreenMesh возвращает меш размером 1 на 1.
Шейдер
Создадим шейдер для получения контура. Он должен содержать два глобальных свойства-текстуры. _OutlineRenderTexture и _OutlineBluredTexture для изображения указанных объектов и его размытого варианта.
Результат работы шейдера для для двух полученных ранее изображений:
Добавляем проход в OutlineFeature
Все действия аналогичны предыдущим проходам.
RenderPassEvent
Осталось указать когда будут вызываться созданные проходы. Для этого каждому из них нужно указать параметр renderPassEvent.
Создадим соответствующее поле в OutlineFeature.
И укажем его всем созданным проходам.
Настройка
Добавим слой Outline и установим его для объектов, которые хотим обвести.
Создадим и настроим все необходимые ассеты: UniversalRendererPipelineAsset и ForwardRendererData.
Результат
Результат для нашего исходного кадра будет следующим!
Доработка
Сейчас обводка объекта будет видна всегда, даже через другие объекты. Чтобы наш эффект учитывал глубину сцены нужно внести несколько изменений.
RenderObjectsPass
Указывая цель нашего рендера, мы должны также указать текущий буфер глубины. Создадим соответствующее поле и метод.
В методе Configure() укажем глубину в настройке цели рендера.
OutlineFeature
В OutlineFeature передадим MyRenderObjectsPass текущую глубину сцены.
UniversalRenderPipelineAsset
В используемом UniversalRenderPipelineAsset поставим галочку напротив пункта DepthTexture.
Результат
Результат без учета глубины:
Результат с учетом глубины:
ScriptableRendererFeature достаточно удобный инструмент для добавления своих проходов в RP.
В нем можно легко заменять RenderObjectsPass’ы и использовать их в других ScriptableRendererFeature. Не нужно сильно углубляться в реализацию Universal RP и менять его код, чтобы что-то добавить.
Для того чтобы общий алгоритм работы со ScriptableRendererFeature и ScriptableRenderPass был более понятен, и чтобы статья не сильно разрослась, я намеренно старался создавать код проходов простым, пусть даже в ущерб их универсальности и оптимальности.
Ссылки
Исходный код — ссылка на gitlab
Модели и сцена взяты из игры Lander Missions: planet depths
За основу примера была взята следующая реализация обводки — ссылка на youtube
Примеры реализации собственных RenderFeature от Unity — ссылка на github.
Серия уроков по созданию собственного ScriptableRenderPipeline. После прочтения становится ясна общая логика работы RP и шейдеров — ссылка на туториалы.










