В первой части мы обсудили процесс настройки автоматического сбора статистики для оптимизации игры на Unreal Engine, предложили различные варианты для этого и показали, как настроить сбор статистики при помощи выбранного нами подхода — размещения статических камер на уровне.
В этой части мы займемся анализом данных, которые удалось собрать при помощи инструментов FPSChart и Unreal Insights. Разберем, как выявить узкие места, влияющие на производительность, определим приоритеты для оптимизации, рассмотрим и проведем действия для оптимизации Render Thread. После этого проанализируем новые данные по производительности после всех изменений.
Планирование бюджета времени
При рендеринге кадра в Unreal Engine его обработка делится между несколькими процессами: подготовка геометрии, расчёт теней, освещение, постобработка и другие. Важно понимать, что полностью убрать задержки невозможно, так как каждый этап требует определённого времени на вычисления. Однако, мы можем оптимизировать распределение нагрузки, чтобы достичь целевой производительности.
Прежде чем анализировать данные по производительности и вносить изменения, необходимо установить целевую производительность и задать детальные параметры распределения времени между процессами. Например, если мы хотим обеспечить работу игры при 60 FPS, то обработка каждого кадра не должна превышать 16.6 миллисекунд. Это время необходимо разделить между подпроцессами.
Для нашего проекта мы использовали приведенное ниже распределение времени внутри каждого потока.
Для потока GPU выделим три ключевые категории и обозначим их целевые показатели:
-
Постобработка (PostProcessing) – 30% (~5 мс)
-
Включает эффекты размытия, глубины резкости, цветокоррекции, отражений и других визуальных улучшений. Эти эффекты могут сильно нагружать GPU.
-
-
Обработка света (Lighting) – 30% (~5 мс)
-
Включает расчёт динамического и статического освещения, теней и глобального освещения.
-
Если источников света слишком много или тени имеют слишком высокое разрешение, этот процесс может занять гораздо больше времени.
-
-
Остальные процессы (BasePass, рендеринг объектов, обработка геометрии) – 40% (~6.6 мс)
-
Обрабатывает геометрию, текстуры, расчёт прозрачности, загрузку данных в память и другие базовые процессы рендеринга.
-
Зависит от сложности сцены, количества полигонов, типа материалов и их шейдеров.
-
Для Render Thread (RT) разделим следующим образом:
-
SceneRenderer::Render – 20% (~3.3 мс)
-
Главный процесс рендеринга сцены.
-
-
FDeferredShadingSceneRenderer::Render – 50% (~8.6 мс)
-
Обработка освещения и теней.
-
-
FRDGBuilder::Execute – 30% (~5 мс)
-
Выполняет граф буферов рендеринга.
Для Game Thread (GT):
-
Обновление акторов – 40% (~6.6 ms)
-
Вызов Tick у всех активных объектов, обработка логики.
-
-
Физика и анимация – 40% (~6.6 ms)
-
Симуляция физики, обработка столкновений и динамики объектов.
-
Движение костей персонажей, blend spaces, IK, физические анимации.
-
-
Остальное – 20% (~3.3 ms)
-
Подготовка рендера, AI, UI, звук и прочее.
-
Вы можете разделить на другие категории или задать другое соотношение.
Результаты профилирования на разных конфигурациях
Мы провели замеры производительности на трёх различных системах с одинаковыми высокими (High) настройками графики. Основной целью было выявить узкие места в рендеринге и определить, какие процессы требуют оптимизации в зависимости от мощности железа.

Для построения графика использовались данные из FPSChart.
Проанализировав представленные на графике данные, мы получили следующую картину.
На слабой системе самым узким местом оказался поток GPU, выполняющий кадр за 50.11 мс, что значительно ниже целевого уровня для комфортного геймплея.
На промежуточной конфигурации основное ограничение сместилось на Render Thread, который выполнялся 22.68 мс, что указывает на высокую нагрузку на процессор.
На мощной системе общее время кадра значительно сократилось, но всё ещё ограничено GPU, который выполняет кадр за 16.17 мс, находясь на грани 60 FPS. При этом общее время кадра превышает 16.6 мс.
Использование данных из FPSChart удобно тем, что в простой форме показывает, какой из основных потоков обрабатывается дольше всего, в отличие от Unreal Insights. В котором, открыв .utrace-файл, вы увидите, например, что для на слабом железе все потоки обрабатывались примерно по 50 мс, но у GT и RT есть долгие Wait задачи, что связано с последовательной работой GT->RT->GPU. Проще говоря, GT и RT ждут GPU, растягивая свою работу на 50 мс.
Полученные данные весьма примечательны. Как мы видим, на разных конфигурациях разные процессы выполняются дольше остальных.
Теперь, зная какой поток на каком железе выполняется дольше всего, посмотрим на данные в Unreal Insights.
В соответствии с методом, выбранном нами в предыдущей части, мы собираем данные с расставленных по уровню камер, и поэтому производительность в некоторых сценах будет хуже, чем усредненные значения на графике выше. Для дальнейшего анализа выберем кадры из тяжелых сцен, поэтому время обработки на приведенных далее скриншотах выше, чем значение на графике.

Рассмотрим подпроцессы для GPU на слабом железе, согласно выбранному бюджету на каждую часть:
-
Постобработка: 6.7 мс против заложенных 5 мс (+34%).
-
Обработка света: 23.5 мс против заложенных 5 мс (+370%).
-
Остальное (BasePath, ShadowDepth, VolumetricFog и др.): 23.8 мс против заложенных 6.6 мс (+360%).

Рассмотрим подпроцессы для RP на среднем железе, согласно выбранному бюджету на каждую часть:
-
SceneRenderer::Render: 3 мс, совпадает с ограничением.
-
FDeferredShadingSceneRenderer::Render: 24.4 мс против 8 мс (+205%).
-
FRDGBuilder::Execute: 11.7 мс против 5 мс (+134%).

Для сравнения рассмотрим подпроцессы для GPU на мощном железе, согласно выбранному бюджету на каждую часть:
-
Постобработка: 2.3 мс против заложенных 5 мс (-56%).
-
Обработка света: 8.5 мс против заложенных 5 мс (+70%).
-
Остальное (BasePath, ShadowDepth, VolumetricFog и др.): 8.6 мс против заложенных 6.6 мс (+30%).
Детально изучив каждое превышение показателей заложенного бюджета, мы выделили следующие проблемы и шаги для их устранения:
-
В Render Thread происходит долгая сборка объектов, которые видны игроку в данный момент, далее следует длительная сборка объектов, отбрасывающих динамические тени.
-
Необходима настройка видимости объектов.
-
Необходимо уменьшить количество объектов, имеющих динамические тени.
-
Для профилирования wait добавить канал task при записи статистики.
-
-
В Render Thread долго работает FRDGBuilder::Execute. Это может быть связано с большим количеством текстур и нагрузкой на VRAM, или большим количеством подготавливаемых draw calls.
-
Оптимизировать размеры текстур.
-
Может помочь настройка видимости объектов..
-
-
В GPU обработка света требует очень много времени.
-
Необходимо перенастроить свет на уровне.
-
Очень много событий обработки волос, даже в сценах, где нет NPC.
-
-
В GPU неожиданно долго обрабатывается стандартный Volumetric Fog.
-
Проверить возможность оптимизации шейдера тумана.
-
-
Замечена обработка nanite, которых вообще не должно быть.
Наши уровни — это закрытые помещения с большим количеством источников света, поэтому мы понимали, что простого способа улучшить его время работы — нет. Изучение методов оптимизации источников света заняло продолжительное время, и по итогу привело к полному изменению системы освещения на уровне. Подробнее об этом мы расскажем в следующей части, а сейчас опишем шаги, которые были сделаны для оптимизации Render Thread.
Оптимизация Render Thread
Оптимизация сборки видимых объектов.
-
В первую очередь мы решили пройти по всем декалям, установив Fade Screen Size. Этот параметр позволяет скрывать декали, когда их размер на экране при рендере будет меньше заданного порога. Важно, что этот параметр зависит от Scale, так что, если вы захотите установить всем декалям одинаковое значение Fade Screen Size, их Scale должен быть одинаковым. Для изменения размера декали рекомендуем использовать Decal Size.
-
Следующим шагом стала установка Cull Distance Volume на весь уровень. Этот объем подсказывает Unreal Engine, какого размера объекты на какой дистанции можно скрыть. Например, если у вас есть много мелких объектов вдали, то, скорее всего, в их отображении нет смысла, поскольку игрок не будет их различать. Это позволит игровому движку значительно сократить количество объектов, которые необходимо обрабатывать и передавать на рендер. Конкретные значения сильно зависят от вашего уровня; при выборе настроек необходимо проверять весь уровень, чтобы избежать внезапно появляющихся объектов. В нашем случае мы выбрали следующие настройки:
-
Size 10, Cull Distance 1000
-
Size 30, Cull Distance 3000
-
Size 60, Cull Distance 4000
-
Size 128, Cull Distance 8000
-
Size 256, Cull Distance 0
-
-
Для дальнейшей оптимизации работы движка по подсчету отображаемых объектов мы установили Precompute Visibility Volume. Как уже упомянуто ранее, наши уровни представляют собой закрытые помещения, поэтому, согласно документации, его использование оправдано для небольших и средних уровней. Он просчитывает видимые камере объекты в процессе запекания света; ценой использования является увеличение требования по памяти.
-
Другой оптимизацией для рендера мелких объектов стало их объединение в Instanced Static Meshes (ISM) — это группировка одинаковых мешей в одного Actor. Такое объединение уменьшает число необходимых draw calls для рендера и снижает нагрузку на видеопамять.
-
Последней настройкой в этом списке отметим HLOD (Hierarchical Level of Detail): её применяют для подмены удаленных от камеры реальных мешей на упрощенные. Мы не использовали ее, но она может пригодиться, если вы строите большие открытые локации.
Для сокращения количества объектов с динамическими тенями необходимо понимать структуру уровня, и будет ли объект двигаться или освещаться движущимся источником света. Для нашего уровня с закрытыми помещениями и большим количеством источников света мы решили использовать запекание света. Наши решения могут не в полной мере подойти для открытых локаций.
-
Таким образом, мы оставили только Static Shadow (раздел Lighting > Advanced для меша) для неподвижных объектов.
-
В настройках источников света можно выставить Max Draw Distance (раздел Performance), ограничив дистанцию для отрисовки теней. Мы не изменяли эту настройку на данном этапе, т.к. понимали, что будем полностью пересобирать освещение на уровне.
Снижение нагрузки от теней.
-
В первую очередь, мы попытались запечь свет, но в процессе Unreal Engine выдал большое количество предупреждений об оверлапе Lighmap UV. Поэтому мы вручную перенастроили размер Lighmap (параметр Lightmap Resolution) для всех мешей на уровне. Для удобства настройки рекомендуем включить режим отображения плотности (Lightmap View Modes > Optimization Viewmodes > Lightmap Density). Для пола, стен и крупных объектов мы добивались зеленого (оптимальный) цветового индикатора, для потолка и мелких мешей — голубого (менее оптимальный).
-
Также для оптимизации запекания света установили Lightmass Importance Volume.
Оптимизация FRDGBuilder::Execute. Возможной причиной долгой работы этого процесса может быть высокая нагрузка на VRAM из-за большого количества текстур или тяжелых текстур. Приведем шаги, которые мы предприняли в дальнейшем.
-
Unreal Engine использует Texture Streaming для динамического изменения размера текстуры при рендере. Вы можете снизить размер текстур, если они избыточны — для проверки удобно использовать режим отображения Required Texture Resolution внутри редактора. Unreal Engine позволяет изменить размер текстуры Maximum Texture Size в настройках файла текстуры (раздел Compression > Advanced). Важно скорректировать размер r.Streaming.PoolSize, чтобы не было эффекта “мыльных” текстур.
-
Убедитесь, что размеры текстур кратны двум (как правило, это так и есть) — это позволяет Unreal Engine использовать MIP-маски для динамического изменения размера текстур.
-
Альтернативой является использование Virtual Texture Streaming. При стандартном Texture Streaming, даже если видна только часть объекта, будет загружена вся текстура целиком, и для определения видимости будет использоваться CPU, и он же будет определять размер MIP. В отличии от этого процесса, Virtual Texture Streaming будет загружать только часть текстуры, видимую в данный момент, при этом GPU, а не CPU, определяет видимую часть объекта. Но материалы, использующие Virtual Texture будут дороже, чем обычные, особенно если в материале используются разные UV. В нашем проекте мы используем оба подхода, в зависимости от сложности материала, размеров и положения меша.
Другой возможной причиной может быть большое число draw calls.
-
Рекомендуем применять группировку мешей ISM, которая описана выше.
Повторный замер производительности
Проведем повторный сбор статистики и проверим, как повлияли наши изменения. Отметим, что на момент сбора приведенных ниже данных не производилась оптимизация света и не были выполнены шаги по оптимизации FRDGBuilder::Execute.

Сразу заметно уменьшение время кадра для слабого железа: среднее время кадра изменилось с 51 мс на 40.35 мс, прирост производительности составил 21%. Снизилось время работы GPU, при этом время работы RT практически равно GPU.
Для среднего железа возросло время RT. Несмотря на то, что оптимизировали именно поток RT, показатели ухудшились с 25.31 мс до 30.06 мс, потеря составила 18%.
Для мощного железа нет значимых изменений в производительности.
Для детального анализа причин такого поведения посмотрим, что показывает Unreal Insight для слабого и среднего железа; для слабого железа дополнительно рассмотрим RT.

Рассмаотрим изменения на слабом железе в потоке GPU, относительно исходных данных и согласно выбранному бюджету на каждую часть:
-
Постобработка: 3.9 мс против заложенных 5 мс, предыдущий 6.7 мс.
-
Обработка света: 16.6 мс против заложенных 5 мс, предыдущий 23.5 мс.
-
Остальное (BasePath, ShadowDepth, VolumetricFog и другое): 26.1 мс против заложенных 6.6, предыдущий 23.8 мс.

Так как поток RT на слабом железе близок по времени исполнения к GPU, рассмотрим его показатели:
-
SceneRenderer::Render: 2.7 мс, заложено 3 мс.
-
FDeferredShadingSceneRenderer::Render: 31.7 мс против 8 мс.
-
FRDGBuilder::Execute: 11.3 мс против 5 мс.

Сравним значения для среднего железа по потоку RT:
-
SceneRenderer::Render: 1.8 мс, против заложенных 5 мс, предыдущий 3 мс.
-
FDeferredShadingSceneRenderer::Render: 39.2 мс против заложенных 8 мс, предыдущий 24.4 мс.
-
FRDGBuilder::Execute: 11.9 мс против заложенных 5 мс, предыдущий 11.7 мс.
Для данного запуска уже был добавлен поток Task в трейсинг, что позволило проверить, что происходит в моменты, обозначенные как WaitForTask:

Большинство источников света на уровне в этот момент были Stationary или Movable. Исходя из этого мы считаем, что запекание света на данном этапе не повлияло на работу с тенями.
Результат оптимизации GPU для слабого железа может быть объяснён уменьшением количества draw calls за счет настроенного Cull Distance Volume и ISM. К сожалению, запись числа draw calls не велась при сборе статистики, но время BasePass уменьшилось в среднем с 3.65 мс до 2.45 мс. Кроме того, время ShadowDepths уменьшилось в среднем с 11.92 мс до 8.73 мс за счет отключения динамических теней у некоторых мешей.
По отзывам членов команды, производивших тест на среднем и мощном железе, в некоторых сценах производительность возросла, а в некоторых наоборот ухудшилась, т.е. амплитуда значений FPS выросла в зависимости от того, какую сцену мы рассматриваем.
Изучив новые данные, мы сохранили план дальнейших улучшений:
-
Произвести описанные шаги по оптимизации FRDGBuilder::Execute.
-
Полностью пересобрать освещение на уровне, предварительно выработав подход к установке источников света, который не будет сильно отличаться от текущего освещения, но будет учитывать возможности по запеканию света и работы с динамическими тенями.
Подводя итоги
В этой части мы рассмотрели подход к оценке производительности каждого потока, изучили результаты наших замеров и предложили шаги по оптимизации Render Thread.
-
Задали бюджет для частей, из которых состоят GT, RT и GPU потоки, чтобы понимать, какая часть нуждается в оптимизации.
-
Рассмотрели и проанализировали, какие действия необходимо предпринять для оптимизации GPU и RT на примере наших данных.
-
Рассмотрели возможности оптимизации Render Thread:
-
Настройка видимости объектов и декалей
-
Оптимизация подготовки количества draw calls для одинаковых объектов
-
Оптимизация теней, настройка Lightmass при помощи режима отображения Light Density
-
Оптимизация текстур при помощи режима отображения Required Texture Resolution
-
Использование Virtual Texture Streaming
-
Хоть мы и получили не совсем тот результат, на который рассчитывали, после оптимизации Render Thread время его работы на среднем железе только значительно выросло. Результаты на слабом железе нас обрадовали, как и участника проекта, который все это время работал при 19-20 FPS.
Кроме того, очевидно, что и на слабом, и на среднем железе проблемой является работа с тенями и светом. Осталось только полностью переделать освещение на уровне, о чем мы расскажем в следующей части.
Дополнительные материалы
-
Про HLOD, ISM, Cull distance в видео
HLODs: Medieval Game Environment extended tutorial -
Managing the Texture Streaming Pool | Tips & Tricks | Unreal Engine
ссылка на оригинал статьи https://habr.com/ru/articles/892472/
Добавить комментарий