Каждый игровой инженер сталкивался с этим. У вас есть 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
Ключевые идеи
-
Глобальный бюджет — вы задаёте максимальное время рендеринга (например, 4 мс на все объекты). Аллокатор сам решает, кому дать высокое качество, а кому — низкое.
-
Importance-скор (важность) — вы задаёте, насколько каждый объект важен для геймплея. Близкий враг с ракетницой важнее, чем декоративный куст.
-
Групповые бюджеты — отдельно лимитируете тени, VFX, врагов. Тени не съедят бюджет врагов.
-
Сглаживание — при смене уровня детализации есть «штраф» и минимальное количество кадров удержания. Нет мерцания при каждой микрооптимизации.
-
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 контролирует поведение оптимизатора:
|
Параметр |
Назначение |
Рекомендуемое значение |
|---|---|---|
|
|
Глобальный бюджет рендеринга (мс) |
3–8 мс (зависит от игры) |
|
|
Лимит CPU времени на оптимизацию |
0.1–0.5 мс |
|
|
Количество итераций улучшения |
1000–5000 |
|
|
Штраф за смену уровня |
0.01–0.05 |
|
|
Максимальное изменение уровня за кадр |
1 |
|
|
Минимальное число кадров удержания уровня |
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/