Оптимизация средства визуализации UE4 для Ethan Carter VR

Как игра с очень богатой графикой, Исчезновение Итана Картера (доступное для Oculus Rift и Steam VR) оказалась сложной задачей для попадания в виртуальную реальность. целевые показатели производительности. Тот факт, что его графическая рабочая нагрузка несколько необычна для Unreal Engine 4 (и, в частности, в значительной степени не похожа на существующие демонстрации UE4 VR), не помог. Я подробно описал причины этого в предыдущем посте; суть этого, однако, заключается в том, что игровой мир Исчезновение Итана Картера статически освещен примерно в 95% областей, а динамическое освещение появляется только в небольших ограниченных пространствах, в помещении.

Важное примечание. Наша (The Astronauts ’) работа значительно предшествует рендереру Oculus VR UE4 . Если бы он был в нашем распоряжении тогда, мне, вероятно, нечего было бы делать с этим портом; но как бы мы были сами по себе. Тем не менее, я настоятельно рекомендую вышеупомянутую статью и код, особенно если ваша игра не соответствует нашему сценарию рендеринга и / или если уловки, которые мы использовали, просто не работают для вас.

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

В статье есть ссылки на Github. Получение ошибки 404 не означает, что ссылка не работает - вам нужно, чтобы ваши учетные записи Unreal Engine и Github были подключены, чтобы увидеть коммиты UE4.

Покажи мне числа

Чтобы подогреть аппетит читателя, сравним графический профиль и тайминги типичного кадра в версии PS4 / Redux с соответствующим из состояния кода VR в мой последний день работы в Астронавты:

Оба профиля были записаны с помощью UE4Editor -game -emulatestereo командной строки в конфигурации Development в системе с графическим процессором NVIDIA GTX 770, с настройками качества игры по умолчанию и разрешением 1920x1080 (960x1080 на глаз). Код игрового процесса был отключен с помощью консольной команды PAUSE, чтобы он не влиял на показания, поскольку это выходит за рамки данной статьи.

Как вы можете (надеюсь) сказать, разница довольно значительная. Хотя большая часть этого была связана с улучшением кода, я должен также воздать должное команде художников The Astronauts - Адаму Брыле, Михалу Косерадскому, Эндрю Познаньскому и Камилу Войцекевичу. все сделали блестящую работу по оптимизации игровых ресурсов!

Этот чрезвычайно простой алгоритм оптимизации, которому я следовал, стал темой на пару месяцев после выпуска Ethan Carter PS4 и стал тем словом, которым нужно жить:

  1. Профилируйте сцену из игры.
  2. Определите дорогие проходы рендеринга.
  3. Если функция не важна для игры, выключите ее.
  4. В противном случае, если мы можем позволить себе потерю качества, откажитесь от настройки.

Путь к виртуальной реальности

Начало порта VR было скромным. Я решил начать переход с версии для PS4 / Redux с упрощения тестирования нашей игры в режиме VR. Как, вероятно, бывает с большинством разработчиков, изначально у нас не было достаточного количества головных телефонов для всех в офисе, и их постоянное включение и выключение было раздражающим. Таким образом, я пришел к выводу, что нам нужен способ подражать одному.

Оказывается, в UE4 уже есть удобный -emulatestereo переключатель командной строки. Хотя он отлично работает в игровом режиме, он не активировал кнопку Играть в VR в редакторе. Я взломал методы FInternalPlayWorldCommandCallbacks::PlayInVR_*(), чтобы также проверить наличие FFakeStereoRenderingDevice в GEngine->StereoRenderingDevice, помимо просто GEngine->HMDDevice. Теперь, хотя это не точно имитирует рабочую нагрузку рендеринга VR HMD, мы могли бы, по крайней мере, получить грубое и быстрое представление о производительности стерео-рендеринга из редактора, не беспокоясь о путанице проводов и разъемов. И оказалось, что по большей части этого хватило.

Пробуя его, Эндрю, наш ведущий художник, заметил, что на время тика игры сильно влияет то, что открыты разные окна редактора. Скорее всего, это накладные расходы из-за того, что редактор запускает большое количество Slate кода пользовательского интерфейса. Сведение к минимуму всех окон, кроме основного фрейма, и установка области просмотра редактора основного уровня в иммерсивный режим, казалось, решила проблему, поэтому я автоматизировал процесс и добавил для него флаг в ULevelEditorPlaySettings. Итак, художники теперь могли переключать его из окна Настройки редактора на свое усмотрение.

Эти изменения, а также некоторые другие, описанные в этой статье, можно просмотреть в моей вилке Unreal Engine на Github (напоминание: вам необходимо подключить учетные записи Unreal Engine и Github, чтобы увидеть UE4. совершает).

Убиваем лишние функции рендерера

В поисках информации о UE4 в VR я обнаружил, что Ник Уайтинг и Ник Дональдсон из Epic Games представили интересную презентацию на Oculus Connect, которую вы можете увидеть ниже.

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

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

Это был первый этап оптимизации, результатом которого стали следующие настройки - отрывок из нашего DefaultEngine.ini:

[SystemSettings]
r.TranslucentLightingVolume=0
r.FinishCurrentFrame=0
r.CustomDepth=0
r.HZBOcclusion=0
r.LightShaftDownSampleFactor=4
r.OcclusionQueryLocation=1
[/Script/Engine.RendererSettings]
r.DefaultFeature.AmbientOcclusion=False
r.DefaultFeature.AmbientOcclusionStaticFraction=False
r.EarlyZPass=1
r.EarlyZPassMovable=True
r.BasePassOutputsVelocity=False

Самый быстрый код - это тот, который не запускается

Напомню, что Ethan Carter - игра со статическим освещением; Вот почему мы могли избавиться от полупрозрачных объемов освещения и ambient occlusion (прямо с его статической долей), поскольку эти эффекты не добавляли ценности игре. По тем же причинам мы могли отключить проход custom depth.

Компромиссы

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

Одним из таких параметров является r.FinishCurrentFrame, который, когда он включен, эффективно создает точку синхронизации ЦП / ГП сразу после отправки кадра рендеринга, вместо того, чтобы помещать в очередь несколько кадров ГП. Это способствует уменьшению задержки движения к фотонам за счет производительности и, кажется, изначально было рекомендовано Epic (см. Слайд выше), но с тех пор они отказались от этого (напоминание: вам необходимо, чтобы ваши учетные записи Unreal Engine и Github были связаны, чтобы увидеть коммиты UE4). Мы отключили его для Ethan Carter VR.

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

С этим связана переменная r.OcclusionQueryLocation, которая управляет точкой в ​​конвейере рендеринга, в которой отправляются запросы окклюзии. Это позволяет балансировать между более точными результатами окклюзии (буфер глубины для тестирования становится более полным после базового прохода) и остановкой ЦП (чем позже отправляются запросы, тем выше вероятность ожидания результатов запроса в следующем кадре) . Изначально рабочая нагрузка рендеринга Ethan Carter VR была связана с процессором (мы наблюдали случайные остановки длительностью в несколько миллисекунд), поэтому перемещение запросов окклюзии до базового прохода было чистым приростом производительности для us, несмотря на небольшое увеличение общего количества вызовов отрисовки (где-то в районе 10–40% для нашей рабочей нагрузки).

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

Чтобы описанный выше трюк сработал, у вас должен быть включен r.EarlyZPass. Переменная имеет несколько различных настроек (подробнее см. В коде); в то время как мы отправили порт PS4 с полным предварительным проходом Z (r.EarlyZPass = 2), чтобы декали D-буфера работали, в версии VR используются только непрозрачные (и немаскированные) окклюдеры (r.EarlyZPass = 1) для экономии вычислительной мощности. Обоснованием было то, что, хотя мы в конечном итоге выполняем больше вызовов отрисовки в базовом проходе и платим немного больше штрафов за затенение из-за более простого Z-буфера, более тонкий предварительный проход сделает это чистым выигрышем.

Мы также остановились на увеличении r.LightShaftDownSampleFactor со значения по умолчанию 2 до 4. Это означает, что разрешение наших масок светового вала составляет лишь четверть от основной цели рендеринга. Световые лучи в этом случае получаются очень размытыми, но это не сильно повлияло на внешний вид игры.

Наконец, я решил отключить новую (на тот момент) функцию UE 4.8 в r.BasePassOutputsVelocity. Сравнивая его производительность с хаком Роландо Калока по внедрению сеток, которые используют смещение мировой позиции в проход скорости, с таймингами предыдущего кадра (которые я ранее интегрировал для порта PS4, чтобы иметь правильное размытие движения и сглаживание листвы), Я обнаружил, что оно просто превосходит новое решение в нашей рабочей нагрузке.

Эксперименты с общей видимостью

Если вас не интересуют сбои, не стесняйтесь переходить к следующему разделу (Создание экземпляров стерео…).

Несколькими абзацами ранее я упоминал сваливание в раннем предварительном проходе Z. Вы также могли заметить в профиле выше, что время отрисовки (то есть время, проведенное в потоке рендеринга) составляло несколько миллисекунд. Это был случай Heisenbug: он никогда не обнаруживался ни в каких внешних профилировщиках, и я думаю, что это связано с тем, что все они сосредотачиваются на изолированных кадрах, а не на их последовательностях, из-за которых межкадровые зависимости заставляют задуматься.

В любом случае, хотя я до сих пор не уверен, что подозрительные тайминги графического процессора и время отрисовки CPU были связаны, я пришел к общепринятому мнению, что игры обычно связаны с процессором, когда дело доходит до рендеринга. Вот почему я взглянул на статистику, которую собирает и отображает UE4, в поисках чего-то, что могло бы помочь мне деконструировать время отрисовки. Это результат STAT INITVIEWS, который показывает подробную информацию о производительности выборки видимости:

Ух ты, почти 5 мс потрачено на отсечение контуров и окклюзии! Это количество вызовов, равное 2, было весьма наводящим на размышления: возможно, я мог бы сократить это время вдвое, поделившись данными набора видимых объектов между глазами?

С этой целью я провел несколько экспериментов. Требовались некоторые приспособления, чтобы заставить движок не запускать код релевантности представления для вторичного глаза, а вместо этого использовать данные первичного глаза. Я добавил рисование усеченной пирамиды отладки к команде FREEZERENDERING, чтобы помочь в отладке отсечения с использованием совместной усеченной пирамиды для обоих глаз. Я улучшил код DrawDebugFrustum(), чтобы лучше обрабатывать матрицы обратной Z-проекции, которые использует UE4, а также чтобы набор плоскостей был источником данных. Было довольно легко заставить один проход отбраковки усеченной кости работать на оба глаза.

Но удаления окклюзии не было.

По причинам производительности, упомянутым ранее, мы застряли с механизмом на основе запросов окклюзии (UE4 использует вариант оригинальной техники). Для проверки требуется существующий предварительно заполненный буфер глубины. Если буфер не соответствует усеченной вершине, объекты будут выбраны неправильно, особенно по краям области просмотра.

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

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

И в какой-то момент какая-то другая оптимизация - и я должен признать, что меня никогда особо не интересовало, какая из них, я просто приветствовала ее - заставила проблему исчезнуть как побочный эффект, и поэтому это стало спорным вопросом:

Стерео инстансинг: не панацея

Epic разработали функцию инстансированного стерео рендеринга для UE 4.11. Epic предоставил нам предварительный доступ к этому коду, и мы с нетерпением ждали его тестирования.

Однако это обернулось разочарованием.

Во-первых, эта функция была специально адаптирована для демонстрации Bullet Train UE4 VR.

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

Но хуже всего было то, что это действительно снизило производительность. У меня больше нет этого кода, чтобы сделать какие-либо свежие тесты, но из моей переписки с Райаном Вэнсом, программистом из Epic, который подготовил для нас патч кода (спасибо ему за инициативу!) :

Сравнение со сборкой до изменений показывает значительное снижение производительности: в сценах без листвы (где мы уже были привязаны к графическому процессору) мы наблюдаем выигрыш ~ 0,7 мс в потоке отрисовки, но потерю ~ 0,5 мс на графическом процессоре .

Однако листва все делает намного, намного хуже (даже после того, как ее починили). Статический блок показывает потерю графического процессора ~ 1 мс при vr.InstancedStereo=0 относительно базового значения и ~ 5 мс при vr.InstancedStereo=1!

Другие разработчики UE4 VR, с которыми я разговаривал, похоже, согласны с этим. Также есть ветка на форумах Unreal с подобными жалобами. Как указывает Райан, это оптимизация процессора, что означает обмен процессорного времени на время графического процессора. Я отказался от этой функции для Ethan Carter VR - к тому моменту мы уже были привязаны к графическому процессору для большей части игры.

Всевидящие глаза

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

Краткое исследование с использованием команды STAT SCENERENDERING показало нам, что количество примитивов было довольно высоким (в районе 4 000–6 000). Однако быстрый осмотр с помощью команды FREEZERENDERING не выявил явных горячих точек, поэтому я выбрал команду VIS . Содержимое Z-буфера после предварительного прохода (но до основного прохода!) Все объясняло.

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

Я просто составлял компоненты ландшафта в специальном корпусе, которые визуализировались как окклюдеры даже когда они используют замаскированные материалы (напоминание: вам нужно иметь подключенные учетные записи Unreal Engine и Github, чтобы увидеть коммиты UE4). Это сократило количество вызовов отрисовки в этой сцене с нескольких сотен до пары тысяч, в зависимости от точного расположения камеры.

Туман такой густой, что его можно было намазать на хлеб

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

Поскольку UE4 отправляет ограничивающие прямоугольники сеток для запросов окклюзии, что делает его несколько грубым и консервативным (т. Е. Подверженным ложным срабатываниям), у нас были большие сетки, которые проходили тесты усеченного отсечения, а затем окклюзию, имея только 1 или 2 пикселя ограничивающего прямоугольника. виден сквозь густую листву. Переход к реальным сеткам на базовом проходе обнаружит, что все их пиксели все равно не пройдут проверку глубины.

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

Это натолкнуло меня на мысль: почему бы не добавить еще одну плоскость к усеченной пирамиде на расстоянии, где непрозрачность тумана приближается к 100%?

Решение уравнения тумана для расстояния и добавление дальней плоскости отсечения сократило еще несколько сотен вызовов отрисовки. У нас был контроль над подсчетом вызовов отрисовки в соответствии с остальной частью игры.

Безумные LODs

В какой-то момент в конце разработки Маттеус Г. Чайдас из AMD смотрел сборку игры и заметил, что мы используем слишком сильно мозаичные деревья в вышеупомянутой начальной сцене. Он был прав: поиск актива в редакторе показал, что размеры экрана LODs 1+ были установлены на смехотворные величины в однозначной процентной области. Другими словами, более низкие LOD практически никогда не сработают.

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

Излишне говорить, что я абсолютно ненавижу, когда художники пытаются неуклюже обходить такие очевидные ошибки, вместо того, чтобы сообщать о них. Я создал тестовую сцену, подтвердил ошибку и начал расследование, и стало очевидно, что экземпляр листвы не принимает во внимание масштабирование экземпляра при вычислении коэффициентов детализации (более того, это даже технически невозможно. без капитального ремонта, так как LOD-фактор рассчитывается для каждого типа листвы на весь кластер). В результате все экземпляры листвы отбраковывались так, как если бы они имели масштаб 1.0, что обычно не подходило для нас.

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

Деньги выстрел

Но самая важная оптимизация - та, которая, как я считаю, сделала все возможное, - это переключение G-буферов во время выполнения. Я должен снова отдать должное Маттеусу Г. Чайдасу за его предложение; Увидев профиль графического процессора в игре, он спросил, можем ли мы уменьшить формат пикселей G-буфера, чтобы уменьшить насыщение полосы пропускания. Я сильно хлопнул себя по лбу. "Почему, конечно, мы действительно можем избавиться от всех из них!"

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

Unreal уже имеет консольную переменную для того, что называется r.GBuffer, только для того, чтобы изменения вступили в силу, требуется перезапуск движка и перекомпиляция шейдеров базового прохода. Я расширил переменную до перечисления, присвоив значение 2 автоматическому контролю выполнения.

Это повлекло за собой кучу небольших изменений во всем движке:

  • Перемещение легкого окклюзии и собирание до прохождения базы.
  • Наличие TBasePassPS условного определения макроса NO_GBUFFER для шейдеров вместо глобальной среды компиляции шейдеров.
  • Создание новой ключевой строки шейдерной карты.
  • Наконец, настройка политик отрисовки для выбора варианта шейдера G-буфера / без G-буфера во время выполнения.

Это изменение сэкономило нам колоссальные 2–4 миллисекунды на кадр, в зависимости от сцены!

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

Ложка дегтя

За исключением конечно, G-буферы постоянно включались. Причем по причинам, которые поначалу были для меня несколько непонятны.

Быстрый сеанс отладки показал, что можно легко расположить себя таким образом, чтобы точечный свет, спрятанный в помещении на другом конце уровня, попадал в пирамиду обзора. Довольно наивная отбраковка света UE4 (простой тест усеченного конуса плюс ограничение доли площади экрана) просто плохо справлялась, и у нас не было возможности даже узнать, что это за источники света.

Я быстро придумал грязную визуализацию в виде новой команды STAT - STAT RELEVANTLIGHTS - которая перечисляет все динамические источники света, видимые в последнем кадре, и проинструктировав художников по ее использованию, Я мог бы предоставить им возможность добавить ручное отключение (переключение видимости) с помощью триггерных томов.

Теперь все, что оставалось оптимизировать, - это время тика игры, но я был уверен, что Адам Биениас, ведущий программист, справится. Я был свободен вымыть свой стол и уйти на новую работу!

Выводы

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

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

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

Проблема несоответствия таймингов / задержек запроса окклюзии раннего прохода Z для каждого глаза требует лучшего понимания. Мне жаль, что у меня не было больше времени, чтобы диагностировать это, и знания, как это сделать, поскольку все обычные методы не смогли точно определить его (или даже обнаружить), и я только начал обнаруживать xperf / ETW и GPUView ».

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

Веселые времена!