Приведи пример интересной ситуации при оптимизации
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Пример оптимизации рендеринга множества объектов в Unity
Одна из самых интересных и сложных ситуаций в моей практике — оптимизация рендеринга 10,000+ анимированных персонажей в мобильной игре с открытым миром. Игра тормозила на 15 FPS на средних устройствах, хотя по логике должно было работать на 60 FPS.
Проблема и анализ
Первоначальная архитектура использовала стандартный подход:
- Каждый персонаж — отдельный
GameObjectсSkinnedMeshRenderer - Анимация через
Animatorи Mecanim - Управление через скрипты на C#
// Исходный неоптимизированный код управления персонажем
public class NPCController : MonoBehaviour
{
private Animator animator;
private NavMeshAgent agent;
void Update()
{
// Логика AI в каждом кадре
UpdateMovement();
UpdateAnimation();
}
}
Профилирование в Unity Profiler показало:
- CPU bottleneck: 35ms на анимацию (Mecanim overhead)
- Draw calls: 5000+ из-за отдельных материалов
- Batch breaking: из-за разных трансформаций и скининга
Решение: многоуровневая оптимизация
1. Инстансинг анимированных мешей
Замена отдельных SkinnedMeshRenderer на GPU Instancing с анимацией в шейдере:
// Вершинный шейдер с анимацией на GPU
v2f vert(appdata_t v, uint instanceID : SV_InstanceID)
{
v2f o;
float4 pos = mul(_BoneMatrices[instanceID][v.boneIndex], v.vertex);
o.vertex = UnityObjectToClipPos(pos);
return o;
}
// Управление инстансингом на C#
public class NPCBatchRenderer : MonoBehaviour
{
private ComputeBuffer boneMatricesBuffer;
private MaterialPropertyBlock propertyBlock;
void Update()
{
// Обновление всех анимаций за один вызов
UpdateAllAnimationsOnGPU();
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, count, propertyBlock);
}
}
2. LOD система с дискретными уровнями
public class NPCLODSystem : MonoBehaviour
{
private enum LODLevel
{
High = 0, // Полная анимация, 500 полигонов
Medium = 1, // Упрощенная анимация, 200 полигонов
Low = 2, // Билборд, 10 полигонов
Culled = 3 // Не рисуется
}
void UpdateLOD(Vector3 cameraPosition)
{
float distance = Vector3.Distance(transform.position, cameraPosition);
currentLOD = CalculateLODLevel(distance);
}
}
3. Job System + Burst Compiler для логики AI
[BurstCompile]
public struct NPCJob : IJobParallelFor
{
public NativeArray<Vector3> positions;
public NativeArray<Vector3> velocities;
public float deltaTime;
public void Execute(int index)
{
// Векторизированные вычисления AI
positions[index] += velocities[index] * deltaTime;
}
}
Результаты и инсайты
После оптимизации:
- FPS вырос с 15 до 55 на том же устройстве
- Draw calls сократились с 5000 до ~200
- Потребление CPU на анимацию упало с 35ms до 4ms
- Потребление памяти снизилось на 40%
Ключевые выводы:
- Профилирование прежде оптимизации — без точных данных из Profiler мы бы оптимизировали не те узкие места
- Архитектурные изменения эффективнее микрооптимизаций — переход на инстансинг дал на порядок больше выигрыша, чем оптимизация отдельных методов
- Мобильные GPU любят инстансинг — это их сильная сторона в отличие от CPU скининга
- Сложность возрастает экспоненциально — каждая оптимизация создавала новые проблемы (например, управление коллизиями для инстансированных объектов)
Самое интересное — это компромиссы: мы пожертвовали индивидуальностью анимаций (все персонажи в одном батче имели одинаковые позы в один момент времени), но получили 4-кратный прирост производительности. В финальной версии мы использовали гибридный подход: ближние персонажи рендерились индивидуально, дальние — инстансированными батчами, что дало и производительность, и визуальное качество.