31 августа 2016 в 14:24

Unity с позиции художника при разработке кроссплатформенной игры из песочницы

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

«Работать в Unity так приятно и удобно: много возможностей для самореализации» — говорили мне, но всё оказалось не так просто. В данной статье не будет петься хвалебных од Unity. Эта статья о суровых реалиях, ограничениях и сложностях, с которыми столкнулась наша маленькая команда инди-разработчиков при создании своей первой, но достаточно крупной игры Death Point.



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

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

Освещение


Наши силы были крайне ограничены и я выбрал тайловую систему. Гибкая, позволяющая достичь хорошего визуального разнообразия, ограниченными средствами. Глядя на свой блеклый уровень, я со слезами на глазах штудировал документацию по Unity в поисках ответа «настройка и работа системы освещения». Документация оказалась достаточно поверхностной и не давала полного понимания о работе системы. Спасла меня документация Unreal Engine, раскрывающая многие аспекты того, как это работает в игровом движке и дающая множество ответов на вопросы “как нужно делать”. Еще пара тройка хороших статей и я был готов творить.

Первым ударом стала невероятная разница между тем, что Unity отображает в редакторе при real-time render и bake. Красивые тени после запекания превращались в кашу, источники света забивали друг друга, их яркость и цвет менялись кардинально.

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

Затем, отсутствие HDR в вариации для мобильных устройств. Везде есть галочки позволяющие его включить, но в результате получалось только жуткое месиво цветов от источника света на объектах, Unity просто не понимает как сохранять light-map в HDR. Как следствие, текстура осветляется до уровня albedo, и на краях освещенной области появляется “радужный” градиент.


Самый ранний тест запеченного света

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


После множества манипуляций — красивый градиент с рисунком

В этой же картинке можно увидеть, что перед лампой стоит 3d объект с прозрачной текстурой, выполняющий роль маски. Все потому, что разработчики не добавили возможность выставлять для baked источника света текстурную маску cookies (форму светового пятна).

Отдельным пунктом можно упомянуть directional-lightmap – прекрасное техническое решение, но из-за мобильной направленности мы не стали его использовать. Сами lightmap не мало весят, а directional-lightmap удваивают это значение. Мы не могли жертвовать таким количеством ресурсов, так как игра должна была хорошо работать и на мобильных устройствах. Причина в том, что весь эффект directional-lightmap виден только в случае, когда источник света попадает в камеру. А таких моментов в top-down игре очень мало.

Размер имеет значение


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

Когда пришло время понять, на что же мы способны, был собран довольно внушительный уровень. И тут снова удар ниже пояса от разработчиков Unity – в консоль падают ошибки о том, что невозможно запечь lightmap, и уровень покрывается черными полосами. Слишком большой уровень и слишком высокие настройки освещения. Жаль.

Вот он, тот уровень, с хорошим освещением, настроенными reflection пробами, light-probe для освещения динамических объектов и с частично реализованным геймплем. Выглядит хорошо, играется отлично. Взгляд падает на количество drawcalls – провал, полный провал… их около двух тысяч.



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

Наш гейм-дизайнер оказался толковым парнем и хорошим программистом. Всего за несколько недель он написал свою систему оптимизации, которая группировала и объединяла меши, создавала атласы из всех объектов используемых в игре и еще кучу разных вещей которых я не понимаю =). В итоге мы добились приемлемого для мобильного приложения количества drawcalls. При максимальной детализации сцены их было не более 250. Все еще много, но хороший телефон справляется на ура, выдавая те самые, заветные, 60 FPS.



После ввода системы оптимизации количество drawcalls стабильно держится в районе 100.

Собственный шейдер


И все же, с картинкой было что-то не так. Результат был далек от ожидаемого и, тем более, желаемого. Я не сразу увидел, что модели выглядят слишком разными в 3D-coat, где я делаю текстуры, и Unity полагая, что все дело в окружении этого объекта.
Несмотря на огромную работу проделанную разработчиками Unity по оптимизации стандартного шейдера, тесты на устройствах показали, что всё не очень хорошо. Да, быстро, но не так как хотелось бы. Да PBR (Physically based rendering), но выглядит странно. Неправильная работа reflection sphere на мобильных устройствах при использовании graphics API 2.0 стала одной из главных причин написания своего шейдера. Переключаться на 3.0 мы не хотели из-за провала в производительности.

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

Вообще PBR в Unity это история PC игр, на котором он чувствует себя хорошо при realtime освещении. В случае с baked освещением и отсутствием направленных лайтмап, PBR становится грудой мусора, которая не понимает, как себя вести. Да, кто то может сказать “Почему нельзя было просто использовать мобильный шейдер в котором зашита cubemap?”. Ответ прост: у нас очень много отражающих поверхностей в закрытых помещениях. Отражения не просто красиво блестят, они подчеркивают форму и дают лучшее понимание и представление о свойствах материала из которого создан предмет, они задают правильную атмосферу того помещения, где находится персонаж. Как бонус, вы можете увидеть то, что не должны были бы видеть: фасады зданий (которые мы не отображаем в основной камере, что бы они не мешали геймплею), потолки помещений, да и просто различную фурнитуру и вывески, которые находятся в недоступном с этого ракурса камеры местах. Всё светится, блестит и отражается, создавая цельную атмосферу и оживляя этот цифровой мир.


Без отражений


С отражениями

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

При написании собственного шейдера мы, изначально, просто скопировали решение юнити – разбили шейдер на 2 части: «металик» и «спекулар». Я далеко не сразу понял, что это именно та часть, где все очень плохо. Это решение не позволяет создавать сложные объекты сочетающие в себе металлические и глянцевые поверхности. Что-то одно на выбор.

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

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

Было создано 2 версии шейдера, один debug, который оперировал всеми возможными текстурными картами по отдельности, и final, оперирующий четырьмя собранными текстурными атласами.


В итоге у нас получилось приличное количество входных данных на debug версии шейдера:

1. Выбор между прозрачность / непрозрачность
2. отключение Normal Map (ее отсутствие на одном объекте дает небольшой прирост производительности, но отключение в больших количествах дает заметный результат). В нашем случае это имеет смысл, так как камера расположена достаточно далеко и не всегда разумно ее использовать, часто добавление АО (ambient occlusion) в albedo показывало заметно лучший результат.
Далее идут различные текстурные карты, которые я могу тут же в шейдере домножить на какой-либо цвет или значение.
3. Albedo
4. Specular
5. Roughness
6. AO
7. Emission
8. Metal
9. Normal map

В процессе компиляции все это многообразие умещается в 4 текстуры.



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

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

Зоны vision персонажа. Туман войны


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



Заключение


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

Есть мысль выгружать готовый уровень обратно в 3D редактор и уже там запекать свет. Это даст огромный прирост как в скорости подготовки сцены, так и в качественном результате. Поверьте, это будет удивительно. Мягкие и жесткие тени, подсветка, качественные градиенты. Думаете только FrostBite может показать все возможности красивого рендера на грани вашего железа? Наша команда можем Вам показать, как должны выглядеть действительно красивые игры на всех устройствах, при все тех же, смешных технических требованиях к железу. Но, сколько бы ни было идей, на их реализацию нужно время. Выход в STEAM может дать нам возможность реализовать все наши амбиции, но для этого нам нужна Ваша помощь!


подробнее с проектом можно ознакомиться тут:
steamcommunity.com/sharedfiles/filedetails/?id=752096160

Статья за авторством Кротова Сергея, при поддержке Чёлушкина Андрея и Ковалевского Виталия.
Кротов Сергей @Serrage
карма
4,0
рейтинг 0,0
Моделлер-аниматор
Самое читаемое Разработка

Комментарии (7)

  • 0
    Мощно Вы, конечно, углубились в освещение. Хотелось бы узнать подробнее как Вы делали туман войны. Спасибо.
    • 0
      К тому, что уже написано в статье, могу добавить что подмена шейдера осуществляется с помощью Camera.SetReplacementShader(), результатом рендера камеры является текстура — карта видимости (как на картинке в статье). Далее эта текстура записывается в глобальную переменную шейдеров (см. Shader.SetGlobalTexture()) откуда ей могут пользоваться любые другие шейдеры, в том числе и ImageEffect, рисующий те самые полоски на экране))
    • 0
      Кроме того, что описал мой коллега, могу добавить еще пару пунктов:
      1. Все трехмерное окружение находится в 2х слоях. Первый слой (геометрия самого уровня) видят обе камеры, второй слой (пропсы, декали, мелкие 3D объекты) рендерит только основная игровая камера.
      Стоит заметить что камера для тумана войны рендерит в 1/4 размера от основной камеры. Тут была сложность, из-за маленького разрешения и заниженных параметров источника света в персонаже, на финальной картинке присутствовал эффект Bias (такие полосы на стенах и поверхности пола.)
      2. Мы влезли в работу источника света и заставили его считывать нормали с геометрии и создавать градиент, который смешивается с основными тенями и маскирует артефакты.
      На картинке очень хорошо виден этот градиент.
  • 0
    Да уж, немаленькую работу вы проделали. Мне только предстоит окунуться во все эти радости и, учитывая, что опыта в подобных делах у меня практически нет, надеюсь, мне так же удастся всё это победить )
  • +1
    В какой-то момент, Unity напрочь отказывалась объединять одинаковые объекты при рендере, отрисовывая их по одному.

    Потому что так работают все лайтпробы — они предназначены только для динамических объектов небольшого размера, на которых батчингом придется пожертвовать. Хочешь батчинг — делай количество вершин в меше <=300 (если без tangent) или <=250 (если с tangent).
    Далее идут различные текстурные карты, которые я могу тут же в шейдере домножить на какой-либо цвет или значение.

    Оно по определению не может быть быстрым — до сих пор карты-середнячки начинают умирать после 4 фетчей из разных семплеров, те суммарно должно быть максимум 4 разных текстуры, но не 7. Как вариант — упаковывать грейскейлы (если данные позволяют) в RGB каналы одной текстуры и потом пробовать сжимать.
    Расскажу об одном из самых из самых гениальных и простых решений которые мы использовали в нашем проекте.
    Туман войны реализован через point-light, находящийся в персонаже и рендерящийся отдельной камерой при максимально простом шейдере, который учитывает только освещение. Результатом является буфер видимости персонажа использующийся для пост-процесса тумана войны и в качествемаски в других шейдерах. Например, в зоне видимости охранников.

    Это самое гениальное решение? Мы точно говорим про мобильные девайсы? Рендерить сцену еще раз с использованием шадоумап, которые работают не на всем зоопарке андроидов? Погоняйте билды на sgs2, nexus7 tegra3 и тп девайсах, что еще вполне можно встретить в качестве «середнячков» — там теней не будет и игрок получит негативный UX из-за того, что сцена будет показана ему не так, как задумал дизайнер.
    Правильная реализация — рендерить видимость в 2 прохода — сначала генерить меш, содержащий вытянутые вертексы от точки обзора и рисовать его с небольшим смещением по высоте с ZWrite On + ColorMask None, а потом тупо рендерить круг геометрии полной видимости со смещением вниз — тогда она будет маскироваться Z-буфером после отрисовки геометрии обстаклов.
    • 0
      Потому что так работают все лайтпробы

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

      Оно по определению не может быть быстрым — до сих пор карты-середнячки начинают умирать после 4 фетчей из разных семплеров, те суммарно должно быть максимум 4 разных текстуры, но не 7. Как вариант — упаковывать грейскейлы (если данные позволяют) в RGB каналы одной текстуры и потом пробовать сжимать.


      Кажется я именно это и написал.
      В процессе компиляции все это многообразие умещается в 4 текстуры.

      Даже больше скажу — 4 текстуры это редкость. Куда чаще только 3.

      Правильная реализация — рендерить видимость в 2 прохода

      Затрудняюсь ответить на твой комментарий достоверно. Но из того что я знаю, это решение было самым первым, что мы реализовали. Негативный опыт. Выглядело хуже, и постоянное дрожание. По производительности все было так же. Одним из тестовых девайсов был Sony Experia L.
      • +1
        вся геометрия уровня статична и не работает с лайтпробами в принципе.

        Тогда еще смешнее получается по рендеру — статическая геометрия вообще плохо работает в юнити в плане рендера (можно проверить фрейм дебаггером как оно криво клеит куски при движении / повороте камеры). А еще батчинг рвется при любых активных лайтах — так устроен рендер юнити — даже если лайты не действуют на рендер — они будут сгруппированы по N-одинаковых лайтов.
        Выглядело хуже, и постоянное дрожание. По производительности все было так же.

        Как оно может дрожать, если это геометрия? Оно будет даже точнее, чем шадоумапы:

        Ну и про производительность — несоизмеримо быстрее. Про тестовые девайсы — перечислил примеры, где оно не будет работать.

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