Как сделать темноту в юнити
#7 Shadows
Содержание
1 Вступление
2 Краткая теория
2.1 ShadowMapping
2.2 Рендер Depth Texture
2.3 Рендер Shadow Map
2.4 Сборка Теней
2.5 Настройка Теней в unity
2.6 Алгоритм фильтрации Percentage Closer Filtering
3 Практика. Пишем шейдер
3.1 Отбрасывание теней
3.2 Получение теней
Вступление
Всем доброго времени суток! За окном декабрь, а значит самое время писать шейдеры! Текущим объектом нашего исследования были выбраны тени. Для понимания того, что тут вообще будет происходить, вам необходимо знать, что такое вертесный и фрагментный шейдеры.
Краткая теория
Начнем мы, как всегда, с теории. Для начала разберемся, что такое тени и какими они бывают в юнити.
Когда на пути светового луча попадается какой-либо объект, то на поверхности, находящийся за ним, образуется темный силуэт этого объекта. В таких случаях мы говорим, что объект отбрасывает тень. В реальном мире переход из темной области в освещенную происходит плавно, образуя полутень (с англ. penumbra ).
Shadow Mapping
Для того, чтобы разобраться, как юнити реализует данный алгоритм, создадим простую сцену: куб, две сферы, бросающих на него тень, камера и источник направленного света (directional light).
Это запрос отрисовки, направленный графическому API. Чем запросов больше, тем выше нагрузка.
Для оптимизации draw call’ы однотипных объектов объединяют (батчат / batching)
Рендер depth texture
Рендер shadow map
Далее по списку идет рендер карты теней, которая используется для shadow mapping’а.
Т.к. directional light распространяется равномерно и независимо от расстояния, то для него будет использоваться ортогональная камера. Плюс ко всем, directinal light олицетворяет собой солнце, а значит, по идее, расстояние от него до каждой точки должно быть одинаковым, а shadow map очень большой. На самом деле это не так. Расстояние для рендера Shadow map выбирается в зависимости от положения камеры.
Вот так примерно выглядит shadow map для нашей сцены.
Сборка теней
В итоге мы имеем такой алгоритм shadow mapping’а в юнити:
Настройка теней
Открываем Edit->Project Settings->Quality
Когда сцена имеет большие размеры, карта теней покрывает огромную территорию и на каждый пиксель карты теней приходится слишком большой участок территории, а следовательно, тени смотрятся очень ступенчато ( этот эффект называют aliasing).
Под теневыми каскадами подразумевают разбиение пространства на каскады в зависимости от удаления до камеры. Каждый каскад уже реализует свою карту теней. Именно для них и рендерится несколько теневых карт с разным разрешением.
Как видно на картинке выше, тень дальней сферы, проходя через границу теневого каскада, немного теряет в качестве.
Это были общие настройки для теней. Также в unity можно настроить отображение теней для каждого отдельного источника света.
Алгоритм фильтрации Percentage Closer Filtering (PCF)
Пишем шейдер
!Переходим! к практике!
Создадим новый материал, для него создадим новый Unlit шейдер и применим его к сфере.
Как видно на изображении ниже, сфера перестала отбрасывать тень, т.к. в unlit шейдере нет её реализации.
Отбрасывание теней (Casting Shadow)
Добавим дополнительный проход в наш шейдер:
В этом проходе нам важна только позиция, поэтому просто переведем положение объекта из object space в clip space.
Ну, собственно, вот и все. Теперь наш шейдер пишет глубину, а это значит, что объект отбрасывает тень.
Но тут возникает небольшая проблема: у нас не производится смещения в зависимости от bias’а и normal bias’а.
Как включить тёмную тему редактора Unity (обновлено)
Приветствую, решился написать об одной наболевшей проблеме, которая затрагивает многих начинающих юнитиводов с ограниченным бюджетом, да и не только. Больше шести лет уже люди просят тёмную тему редактора Unity сделать бесплатной, но как мы видим, воз и ныне там.
Подготовка
Предполагается, что Unity уже установлен. Иначе зачем любопытный %UserName% читает эту статью? Обычно исполняемый файл редактора 64-битной версии Unity располагается по пути:
Будем использовать этот путь для примера, поэтому скорректируйте под свою ситуацию в случае отличия. Настоятельно рекомендую сделать резервную копию! Чтобы потом не терять время на переустановку или поиск оригинального файла если пойдёт что-то не так, как задумано:
Затем скачиваем и распаковываем себе в любое удобное место x64dbg из раздела Releases. На момент написания статьи, самой новой была версия «snapshot_2017-12-26_13-39.zip». Запускаем x64dbg с помощью лаунчера x96dbg или же напрямую из его подкаталога:
Меняем одно условие
Итак, пришла пора действовать. Открываем меню File => Open (также горячая клавиша F3 или первая пиктограмка на панели инструментов) и шагаем в каталог установленного Unity, выбираем всё тот же исполняемый файл редактора «Unity.exe» о котором упоминалось выше, получим примерно следующее:
Переходим на вкладку Symbols и слева в списке модулей выбираем unity.exe:
Интерфейс может чуток притормаживать при обработке больших списков, поэтому терпеливо пишем в поле Search строку «GetSkinIdx»:
Двойным кликом по строке результата поиска переходим по его адресу на вкладку CPU:
Здесь нас интересует инструкция jne. Опять таки двойным кликом по ней открываем диалоговое окно, в котором меняем эту инструкцию на je, остальное не трогаем:
После применения правки, открываем контекстное меню и выбираем Patches (Ctrl+P):
При помощи кнопки Patch File сохраняем новый Unity.exe временно куда-нибудь, потому что текущий файл занят отладчиком:
Проверка результата
Поздравляю, всё выполнено успешно. Остаётся лишь закрыть отладчик и перезаписать оригинальный Unity.exe тем файлом, который сохранили при помощи Patch File. Убедиться в том, что не затронули случайно чего лишнего, можно сравнив по содержимому резервную копию с новым файлом, должен отличаться только один байт и больше ничего:
Запускаем и наслаждаемся тёмной темой оформления:
Вполне вероятно, что в Unity перепрячут тёмную тему и описанный тут метод перестанет работать. Но как известно, против лома нет приёма. Также может наоборот произойти чудо, наконец-то прислушаются к пользователям и таки сделают её бесплатной. Если устали ждать и хочется ещё больше кастомизации — рекомендую присмотреться к Zios Themes: описание на форуме Unity + исходники на GitHub.
Как я делал 2D тени в Unity
Что первое приходит в голову разработчику инди-игры, когда он сталкивается с необходимостью добавления фичи, представления о реализации которой толком не имеет? Разумеется, он идёт искать следы тех, кто уже прошёл этот путь и удосужился записать свой опыт. Так поступил и я некоторое время назад, приступая к созданию теней в своей игре. Найти нужную информацию — в виде статей, уроков и гайдов — не составило особого труда. Однако, к моему удивлению, я обнаружил, что ни одно описанное решение мне попросту не подходит. Поэтому, реализовав своё собственное, я решил поведать о нём миру.
Стоит предупредить заранее, что данный текст не претендует на бытие неким ультимативным гайдом или мастер-классом. Использованный мною метод может быть не универсальным, далеко не самым эффективным и не закрывающим задачу создания двухмерных теней в полной мере. Это скорее история о том, к каким ухищрениям пришлось прибегнуть неопытному разработчику в моём лице для достижения удовлетворяющего его требованиям результата.
Сам результат перед вами:
А подробности пути к его достижению ждут вас под катом.
Постановка задачи
Итак, на момент принятия решения о добавлении в игру теней, у меня имелось:
Для всего этого необходимы были самые простейшие тени, повторяющие контуры объекта, и отбрасываемые от единственного фиксированного источника освещения.
При этом следовало трепетно отнестись к производительности. Ввиду специфики жанра и особенностей его реализации, большинство объектов, отбрасывающих тени, в каждый момент времени находятся непосредственно на экране. А их общее число может составлять больше сотни, если говорить об игровых сущностях, и пару тысяч, если говорить об отдельных спрайтах.
Реализация
Собственно, основная загвоздка оказалась в том, что Dwarfinator, грубо говоря — 2,5D игра. Подавляющее большинство объектов существует в двухмерном пространстве с осями X и Y, а ось Z используется крайне редко. Визуально же, и отчасти, геймплейно, ось Y используется для отображения как высоты, так и глубины, разделяясь таки образом на виртуальные оси Y и Z. Использовать в такой ситуации для создания теней стандартные средства Unity не представлялось возможным.
Но на самом деле, честного освещения мне и не требовалось, достаточно было иметь возможность вручную создавать тень для каждого объекта. Поэтому самое простое, что мне пришло в голову — просто разместить позади каждой сущности её копию, повёрнутую в трёхмерном пространстве так, чтобы имитировать нахождение на поверхности локации. Всем спрайтам такой псевдотени задавался чёрный цвет, при этом сохранялась иерархическая структура хозяина тени, что позволяло синхронно с хозяином анимировать её таким же аниматором.
Выглядела такая синхронная анимация примерно так:
Однако тени требовалась прозрачность. Самым простым решением было задать её каждому спрайту тени. Но такая реализация выглядела неудовлетворительно — спрайты накладывались друг на друга, образуя в месте наложения менее прозрачные области.
Стало очевидно, что прозрачность нужно накладывать на тень как на цельный объект. Первым экспериментом на эту тему стало навешивание на тень камеры, отрисовывающей эту тень в RenderTexture, которая потом уже использовалась в качестве материала прикреплённым к родителю тени Plane. Ему уже можно было без проблем задавать прозрачность. Сами же тени находились за пределами кадра, чтобы избежать наложения друг на друга областей захвата камер. Подход работал, но оказалось, что уже пара десятков теней вызывает серьёзные проблемы с производительностью, преимущественно, из-за числа находящихся на сцене камер. Кроме того, ряд анимаций предполагал значительное перемещение отдельных спрайтов моба в рамках его корневого объекта, из-за чего в поле зрения камеры должна была находиться область, значительно превышающая размер реального изображения в отдельный момент времени.
Решение нашлось быстро — если нельзя отрисовывать каждую тень отдельной камерой — почему бы не отрисовывать одной камерой все тени? Всё, что нужно было сделать — отвести под тени отдельную область сцены, несколько выше поля зрения основной камеры, направить на эту область дополнительную камеру, и отображать её вывод между локацией и остальными сущностями.
Ниже можно наблюдать пример вывода этой камеры:
Производительность от такой реализации страдала гораздо меньше, так что решение было сочтено рабочим и применено ко всем мобам, статичным объектам и снарядам. Далее последовала очередь спрайта локации. Использовать один спрайт на все объекты, как это было реализовано ранее, оказалось невозможным. Использование копии объекта в качестве его же тени хорошо работает только до тех пор, пока объект полностью плоский. Уже при создании теней для мобов было заметно, что разнесённые по третьей координате точки соприкосновения с поверхностью нарушают корректность тени относительно этих точек.
Следующий скриншот демонстрирует пример такого нарушения. За точку соприкосновения с поверхностью принята пятка моба, но тени ступней уже выходят за рамки самих ступней.
И если в случае с ногами огра ещё можно немного изменить положение тени и замаскировать проблему, то для нескольких десятков стволов деревьев на это нет и шанса. Все объекты локации, которые должны были отбрасывать тень, следовало сделать отдельными GameObject. Именно так я и поступил, разместив на префабе локации экземпляры соответствующих разрушаемых объектов и отключив им неиспользуемые в таком положении скрипты. Заодно благодаря этому стало возможным включить их в общую сортировку объектов сцены, и улетающие за пределы локации снаряды больше не отрисовывались строго поверх всех объектов, а пролетали между ними. К тому же стало возможным сделать сами объекты анимированными.
Но тут меня поджидала новая неприятность. С тенями и десятками новых объектов максимальное количество одновременно находящихся на сцене GameObject, а вместе с ними и компонентов Animator и SpriteRenderer, возросло более чем в два раза. Когда я выпускал на локацию всю волну мобов, что составляло порядка 150 штук, Profiler укоризненно показывал мне примерно 40мс, уходивших только на рендеринг и анимацию, а частота кадров в целом колебалась в районе 10. Я отчаянно оптимизировал собственные скрипты, борясь за каждую миллисекунду, но этого было недостаточно.
В поисках дополнительных средств оптимизации, я набрёл на просторах документации и гайдов на динамический батчинг.
Frame Debugger показывал, что батчатся у меня, в лучшем случае, детали каждого объекта или моба по отдельности. Создав для первых и для вторых по атласу спрайтов, я добился батчинга теней до всего нескольких вызовов отрисовки, но владельцы этих теней батчиться между собой упорно отказывались.
Эксперименты на отдельной сцене показали, что динамический батчинг ломается при наличии у объектов компонента SortingGroup, который я использовал для сортировки отображения сущностей на экране. Обойтись без него, в теории, было возможно, однако выставление значений сортировки для каждого спрайта и системы частиц в объекте по отдельности могло выйти ещё дороже, чем отсутствие батчинга.
Но кое-что мне не давало покоя. Объект-тень, являясь в реальной сцене потомком объекта-хозяина, технически входил в тот же самый SortingGroup, однако с динамическим батчингом объектов-теней проблем не возникало. Единственное отличие было в том, что объекты-хозяева отрисовывались основной камерой сразу на экран, а объекты-тени — сначала в RenderTexture.
В этом и оказалась загвоздка. Что именно является причиной такого поведения — интернету неведомо, но при рендеринге камерой изображения в RenderTexture, SortingGroup больше не ломали батчинг. Решение казалось весьма странным, нелогичным, и вообще самым что ни на есть костылём. Но реализовав рендеринг сущностей тем же методом, что и рендеринг теней, и получив таким образом помимо слоя теней, ещё и слой сущностей, я добился уже вполне приемлемых значений производительности.
Скриншот ниже демонстрирует пример отрисовки слоя сущностей.
Итого в общем случае рендеринг некоторой сущности в координате Y выглядит так:
На скриншоте ниже камера редактора переведена в трёхмерный режим для демонстрации расположения слоёв относительно друг друга.
Нюансы
Но как выяснилось в процессе тиражирования решения на другие сущности, общий случай не покрывал все возможные сценарии. Например, существовали сущности, находящиеся на некоторой высоте относительно поверхности, в частности, снаряды и некоторые персонажи катсцен. Кроме того, снаряды ещё и имели свойство поворачиваться в зависимости от направления своего движения по экрану, из-за чего помимо выставления им точки пересечения объекта и его тени, понадобилось выделять вращаемую часть в отдельный дочерний объект, править логику вращения снаряда и их анимации.
Следующий скриншот показывает пример вращения снарядов и их теней.
Летающие персонажи же, как и запланированные летающие мобы, могут вдобавок перемещаться в рамках своей виртуальной Y координаты, что потребовало создания механизма расчёта положения тени из положения её хозяина на виртуальной оси Y.
На гифке ниже изображён пример перемещения объекта по высоте.
Другим выбивающимся из общей концепции случаем оказался танк. В отличие от всех остальных сущностей, танк имеет весьма существенный размер по виртуальной оси Z, а общая реализация теней, как уже упоминалось, требует, чтоб объект был практически плоским. Самым простым способом это обойти были вручную нарисованные формы теней для отдельных деталей танка, благо поместить на слой теней можно было что угодно.
Для корректного построения рисованных вручную теней мне пришлось на основе скриншота уже существующей тени собрать конструкцию из линий, которую можно наблюдать на скриншоте ниже.
Если отмасштабировать и разместить эту конструкцию таким образом, что верхняя часть будет находиться в некоторой точке родительского объекта, а нижняя — в точке его соприкосновения с поверхностью, правый угол конструкции покажет место, в котором должна находиться соответствующая точка тени. Спроецировав таким образом несколько ключевых точек, не составляет особого труда построить по ним всю тень.
Помимо этого, отдельные детали танка могли иметь разную высоту крепления дочерних деталей, что, как и в случае с летающими персонажами и мобами, требовало подстройки положения тени каждой конкретной детали.
На скриншоте ниже показан танк, его тень в сборе и она же в виде отдельных частей.
Отдельной болью оказались тени стен. На момент начала работы над тенями стены имели ту же природу, что и детали танка — один объект из нескольких десятков отдельных спрайтов. Однако при этом у стен имелось по нескольку состояний, управляемых аниматором.
Крепко задумавшись, что с ними делать, я пришёл к выводу, что концепцию стен необходимо менять. В результате стены были разделены на секции, каждая из которых имеет собственный набор состояний, собственный аниматор и собственную тень. Это позволило использовать для параллельных оси X секций такой же подход к созданию теней, как и с мобами, а для тех секций, что не подходили под это правило уже придумывать что-то своё. В отдельных случаях пришлось создавать для тени секции собственный аниматор и вручную задавать положение спрайтов.
Например, в случае секции, отображённой на скриншоте ниже, тень сделана путём применения искажения для каждого отдельного бревна вместо всей секции целиком.