10 000 объектов за 0.083 мс: как распределить бюджет рендеринга без просадок FPS

от автора

Каждый игровой инженер сталкивался с этим. У вас есть 16.67 мс на кадр (60 FPS). В сцене 10 000+ объектов: враги, частицы, тени, декали, постыффекты. Нужно решить: на что потратить бюджет, чтобы игрок видел самое важное в максимальном качестве, а FPS не проседал?

Типичные подходы и их недостатки

1. Дистанционный LOD (расстояние до камеры)

  • Проблема: враг в 50 метрах за прицелом важнее, чем дерево в 10 метрах за спиной. Расстояние не учитывает семантику.

2. Глобальный пресет качества (низкий/средний/высокий)

  • Проблема: босс и фоновая крыса получают одинаковый уровень детализации.

3. Ручная настройка с приоритетами

  • Проблема: не масштабируется на тысячи объектов. Динамическая камера или смена игрового момента ломают статическую конфигурацию.

4. Фиксированные лимиты на категории (например, не более 10 NPC с полным освещением)

  • Проблема: в момент, когда вокруг игрока 12 врагов, система не знает, кого понизить — оба варианта плохи.

Результат: дёргающийся FPS, визуальный «мусор» (то высокий LOD, то низкий), бесконечные танцы с настройками.

Требования к решению

  • Глобальная оптимизация, а не локальные эвристики.

  • Учёт важности объектов (gameplay > фон).

  • Поддержка групповых бюджетов (тени, VFX, враги).

  • Сглаживание смены уровней (нет мерцания).

  • Zero-аллокации на кадр.

  • Работа для 10 000+ объектов за <1 мс.

Решение: глобальный оптимизатор бюджета рендеринга

AgiqRenderBudget — библиотека для .NET 8.0+, которая за ~0.083 мс распределяет качество для 10 000 объектов, соблюдая глобальные и групповые бюджетные ограничения.

dotnet add package AgiqRenderBudget --version 1.0.1

Ключевые идеи

  1. Глобальный бюджет — вы задаёте максимальное время рендеринга (например, 4 мс на все объекты). Аллокатор сам решает, кому дать высокое качество, а кому — низкое.

  2. Importance-скор (важность) — вы задаёте, насколько каждый объект важен для геймплея. Близкий враг с ракетницой важнее, чем декоративный куст.

  3. Групповые бюджеты — отдельно лимитируете тени, VFX, врагов. Тени не съедят бюджет врагов.

  4. Сглаживание — при смене уровня детализации есть «штраф» и минимальное количество кадров удержания. Нет мерцания при каждой микрооптимизации.

  5. Zero-аллокации после прогрева — все внутренние буферы переиспользуются.

Чем отличается от встроенных решений

Многие разработчики полагаются на стандартные механизмы своих движков: Unity LOD Group, Unreal LOD, Godot LOD. AgiqRenderBudget решает ту же задачу, но с принципиально другим подходом.

Характеристика

Встроенные LOD (Unity/Unreal)

AgiqRenderBudget

Принцип работы

Локальный (дистанция до камеры)

Глобальный (оптимизация по всем объектам)

Учёт важности объектов

❌ (только расстояние)

✅ (importance, gameplay > фон)

Групповые бюджеты

❌ (все объекты в одной куче)

✅ (тени, VFX, враги — отдельные лимиты)

Динамическое перераспределение

❌ (каждый объект сам за себя)

✅ (ресурсы уходят туда, где важнее)

Сглаживание смены LOD

❌ (мгновенное переключение)

✅ (штраф + мин. кадры удержания)

Контроль над бюджетом

❌ (нет точного лимита в мс)

✅ (вы задаёте budgetMs)

Zero-аллокации на кадр

N/A

Платформа

Только свой движок

Любой .NET (Unity, Godot, Monogame, самописный)

Почему встроенных LOD недостаточно

1. Они не знают, что важно для геймплея

Встроенный LOD смотрит только на расстояние. Куст в 5 метрах получает максимальную детализацию. Босс в 20 метрах — среднюю. Абсурд, но так работают все дистанционные LOD.

AgiqRenderBudget позволяет задать Importance — число от 0 до 1, которое вы определяете сами. Важность может учитывать:

  • тип объекта (босс > рядовой враг > куст)

  • участие в геймплее (цель квеста > фон)

  • текущее состояние (объект под прицелом > объект за спиной)

2. Они не умеют распределять ресурсы между разными категориями

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

AgiqRenderBudget поддерживает GroupBudget. Вы выделяете каждой категории свой лимит:

var groups = new[]{    new GroupBudget(1, 0.45f), // тени: максимум 0.45 мс    new GroupBudget(2, 0.35f), // VFX: 0.35 мс    new GroupBudget(3, 0.55f)  // враги: 0.55 мс};

3. Они не дают контроля над суммарным бюджетом

Встроенные LOD не позволяют сказать: «Я готов потратить на рендеринг всех объектов не более 4 мс». Они просто переключают уровни по дистанции, а суммарное время может быть каким угодно.

AgiqRenderBudget работает в режиме жёсткого бюджетного ограничения. Алгоритм подбирает уровни качества так, чтобы суммарная стоимость не превышала BudgetMs, максимизируя при этом sum(importance × quality).

4. Они дёргаются при переключении

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

AgiqRenderBudget использует сглаживание: штраф за смену уровня (ChangePenalty) и минимальное количество кадров удержания (MinHoldFrames). Это предотвращает бесконечные переключения туда-сюда.

5. Они привязаны к конкретному движку

Unity LOD Group работает только в Unity. Unreal LOD — только в Unreal. Если вы пишете игру на Monogame, Godot или на самописном движке, встроенных LOD просто нет.

AgiqRenderBudget написан на чистом .NET и работает везде, где есть .NET 8.0+ и ваша система рендеринга. Вы сами решаете, как интерпретировать QualityOption.cost и как применять выбранный уровень качества.

Быстрый старт

Шаг 1. Создание оптимизатора и состояния

using AgiqRenderBudget;var optimizer = new AgiqRenderBudgetOptimizer(seed: 123);var state = new RenderBudgetState(); // хранит историю уровней между кадрами

Шаг 2. Определение объектов рендеринга

Каждый объект описывается:

  • Importance — насколько объект важен (0.0 – не важен, 1.0 – критичен)

  • GroupId — категория (тени, VFX, враги, UI)

  • MinLevel — минимальный уровень качества

  • PinnedLevel — закреплённый уровень (например, для босса или интерфейса)

  • Options[] — массив вариантов качества со стоимостью (в мс) и качеством

var items = new List<RenderItem>();foreach (var obj in scene.Objects){    items.Add(new RenderItem    {        Id = obj.Id,        Importance = obj.IsEnemy ? 0.9f :                      obj.IsDecorative ? 0.1f : 0.5f,        GroupId = obj.Type == "Shadow" ? 1 :                  obj.Type == "VFX" ? 2 : 3,        MinLevel = obj.IsBoss ? 2 : 0,        PinnedLevel = obj.IsUI ? 3 : -1,        Options = new[]        {            new QualityOption(cost: 0.005f, quality: 0.20f),            new QualityOption(cost: 0.012f, quality: 0.45f),            new QualityOption(cost: 0.025f, quality: 0.75f),            new QualityOption(cost: 0.045f, quality: 1.00f)        }    });}

Шаг 3. Определение групповых бюджетов

var groups = new[]{    new GroupBudget(groupId: 1, budgetMs: 0.45f), // тени    new GroupBudget(groupId: 2, budgetMs: 0.35f), // VFX    new GroupBudget(groupId: 3, budgetMs: 0.55f)  // враги};

Шаг 4. Подготовка контекста (один раз при загрузке сцены)

var ctx = optimizer.Prepare(items, groups);

Шаг 5. Оптимизация каждый кадр

var settings = PlatformProfile.PC60.ToSettings(    seed: 123,    improveCap: 2500,    randomMoves: 2);var allocation = optimizer.OptimizeFrame(ctx, settings, state);for (int i = 0; i < allocation.Levels.Length; i++){    scene.Objects[i].SetQualityLevel(allocation.Levels[i]);}

Полный рабочий пример

using AgiqRenderBudget;public class RenderBudgetManager{    private AgiqRenderBudgetOptimizer _optimizer;    private RenderBudgetState _state;    private object _ctx;    private Scene _scene;    public RenderBudgetManager(Scene scene)    {        _scene = scene;        _optimizer = new AgiqRenderBudgetOptimizer(seed: 123);        _state = new RenderBudgetState();                BuildItems();    }    private void BuildItems()    {        var items = new List<RenderItem>();                foreach (var obj in _scene.Objects)        {            float importance = obj.IsEnemy ? 0.9f :                                obj.IsVFX ? 0.6f :                                obj.IsDecorative ? 0.2f : 0.5f;                        int group = obj.Type == "Shadow" ? 1 :                        obj.Type == "VFX" ? 2 :                        obj.IsEnemy ? 3 : 4;                        items.Add(new RenderItem            {                Id = obj.Id,                Importance = importance,                GroupId = group,                MinLevel = obj.IsBoss ? 2 : 0,                PinnedLevel = obj.IsUI ? 3 : -1,                Options = new[]                {                    new QualityOption(0.005f, 0.20f),                    new QualityOption(0.012f, 0.45f),                    new QualityOption(0.025f, 0.75f),                    new QualityOption(0.045f, 1.00f)                }            });        }                var groups = new[]        {            new GroupBudget(1, 0.45f),            new GroupBudget(2, 0.35f),            new GroupBudget(3, 0.55f),            new GroupBudget(4, 2.50f)        };                _ctx = _optimizer.Prepare(items, groups);    }        public void Update()    {        var settings = PlatformProfile.PC60.ToSettings(123, 2500, 2);        var allocation = _optimizer.OptimizeFrame(_ctx, settings, _state);                for (int i = 0; i < allocation.Levels.Length; i++)        {            _scene.Objects[i].SetQualityLevel(allocation.Levels[i]);        }    }}

Производительность

Тестовый стенд: Intel Core i5-11400F, Windows 11, .NET 8, BenchmarkDotNet

Количество объектов

Среднее время

Аллокации

1 000

6.37 µs

0 B

5 000

31.87 µs

0 B

10 000

83.62 µs

0 B

10 000 объектов за 0.083 мс — это ~0.5% от кадра (при 60 FPS).

Оставшиеся 16.5 мс — на рендеринг, физику, анимации и логику. Оптимизатор не становится узким местом.

Настройка качества (QualityOption)

Каждый объект предоставляет массив дискретных вариантов качества:

Options = new[]{    new QualityOption(cost: 0.005f, quality: 0.2f), // LOD 0: очень грубо    new QualityOption(cost: 0.012f, quality: 0.45f), // LOD 1: средне    new QualityOption(cost: 0.025f, quality: 0.75f), // LOD 2: хорошо    new QualityOption(cost: 0.045f, quality: 1.0f)   // LOD 3: максимально}
  • cost — стоимость рендеринга в миллисекундах (должна быть измерена на целевой платформе).

  • quality — абстрактная «полезность» (от 0 до 1). Оптимизатор максимизирует сумму importance × quality при соблюдении бюджетов.

Сглаживание и настройки

BudgetSettings контролирует поведение оптимизатора:

Параметр

Назначение

Рекомендуемое значение

BudgetMs

Глобальный бюджет рендеринга (мс)

3–8 мс (зависит от игры)

TimeLimitMs

Лимит CPU времени на оптимизацию

0.1–0.5 мс

ImproveIterationsCap

Количество итераций улучшения

1000–5000

ChangePenalty

Штраф за смену уровня

0.01–0.05

MaxStepPerFrame

Максимальное изменение уровня за кадр

1

MinHoldFrames

Минимальное число кадров удержания уровня

3–5

Пример для PC (60 FPS):

var settings = new BudgetSettings(    budgetMs: 4.5f,    timeLimitMs: 0.25f,    improveIterationsCap: 2500,    changePenalty: 0.03f,    maxStepPerFrame: 1,    minHoldFrames: 4);

Пример для мобильного устройства:

var settings = new BudgetSettings(    budgetMs: 2.5f,    timeLimitMs: 0.15f,    improveIterationsCap: 1000,    changePenalty: 0.05f,    maxStepPerFrame: 1,    minHoldFrames: 3);

Интеграция с Unity

using UnityEngine;using AgiqRenderBudget;public class RenderBudgetController : MonoBehaviour{    private AgiqRenderBudgetOptimizer _optimizer;    private RenderBudgetState _state;    private object _ctx;    private LODGroup[] _lodGroups;        void Start()    {        _optimizer = new AgiqRenderBudgetOptimizer(123);        _state = new RenderBudgetState();                _lodGroups = FindObjectsOfType<LODGroup>();        var items = BuildRenderItems(_lodGroups);                _ctx = _optimizer.Prepare(items, CreateGroupBudgets());    }        void Update()    {        var settings = PlatformProfile.PC60.ToSettings(123, 2500, 2);        var allocation = _optimizer.OptimizeFrame(_ctx, settings, _state);                for (int i = 0; i < allocation.Levels.Length; i++)        {            SetLODLevel(_lodGroups[i], allocation.Levels[i]);        }    }}

Бесплатное тестирование — без ограничений. Коммерческое использование требует лицензии.

NuGet: https://www.nuget.org/packages/AgiqRenderBudget

GitHub (бенчмарки): https://github.com/likeslines-maker/agiq-render-budget

AgiqRenderBudget решает задачу, с которой не справляются встроенные LOD Unity и Unreal: глобальное распределение бюджета рендеринга между тысячами объектов с учётом их важности для геймплея.

В отличие от дистанционных LOD, библиотека:

  • Учитывает важность объектов (gameplay > фон).

  • Поддерживает групповые бюджеты (тени, VFX, враги).

  • Даёт контроль над суммарным бюджетом в миллисекундах.

  • Сглаживает смену уровней (нет мерцания).

  • Работает на любом .NET-движке (Unity, Godot, Monogame).

Если ваша игра страдает от нестабильного FPS или визуального «мусора» — попробуйте глобальную оптимизацию вместо локальных эвристик.

ссылка на оригинал статьи https://habr.com/ru/articles/1042216/