Оптимизация рендеринга мобильной игры
На Samsung Galaxy A52 (Adreno 618) игра держит 28–32 FPS при целевых 60. На Xiaomi Redmi Note 11 с Helio G96 и Mali-G57 — стабильные 55–60 FPS. Разная производительность на похожих по цене устройствах — типичная ситуация для мобильного геймдева. Причина почти всегда в том, что оптимизация проводилась на флагмане, а mid-range GPU с другой архитектурой даёт совсем другую картину.
Диагностика: что тормозит
CPU-bound vs GPU-bound
Прежде чем оптимизировать — понять что является bottleneck. В Unity: Frame Debugger + Profiler. Включаем Profile GPU в Android Player Settings и смотрим Profiler → GPU. Если GPU-время на кадр близко к 16ms (60 FPS) а CPU-время значительно меньше — GPU-bound. Если наоборот — CPU-bound.
На Unreal Engine: stat GPU в консоли, ProfileGPU команда, RenderDoc для захвата кадра. r.ScreenPercentage 50 — быстрый тест: если FPS резко вырос при снижении разрешения вдвое — точно GPU-bound.
// Unreal console commands для диагностики
stat unit // общий breakdown CPU/GPU/frame time
stat drawcalls // количество draw calls текущего кадра
r.ShowFlag.Rendering 1 // включаем detailed rendering stats
Draw calls
На мобильных GPU draw call overhead выше чем на консолях/PC. 500+ draw calls в кадре — красная зона для mid-range Android. Каждый уникальный материал = отдельный draw call. Каждый MeshRenderer с уникальным материалом = ещё один.
Static batching (Unity): объекты с одинаковым материалом объединяются в один mesh. Требование — одинаковый Material asset (не просто одинаковые настройки). Mark as Static в Inspector. Работает автоматически при сборке.
GPU Instancing: для повторяющихся объектов (трава, деревья, враги одного типа):
// Материал должен поддерживать instancing
material.enableInstancing = true;
// Рисуем 1000 экземпляров одним draw call
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1000);
SRP Batcher (Unity URP/HDRP): автоматически батчит объекты с разными материалами но одинаковым шейдером. Включается в URP Asset → SRP Batcher = enabled. Самый простой способ сократить draw calls без ручного batching.
Шейдеры для мобильных GPU
Tile-based rendering (TBIMR)
Мобильные GPU (Adreno, Mali, PowerVR, Apple) используют Tile-Based Immediate Mode Rendering. Экран делится на тайлы, каждый рендерится целиком в быстром on-chip памяти. Это означает:
-
Framebuffer fetch— чтение из текущего framebuffer внутри тайла — практически бесплатно. Используем для deferred lighting:gl_LastFragDataв GLSL (GLES extensionEXT_shader_framebuffer_fetch). -
Depth pre-passна мобиле — часто лишний overhead, TBIMR и так эффективно работает с depth test внутри тайла. -
Discardв фрагментном шейдере (alpha-test, clip) — на tile-based GPU убивает ранний depth test для целого тайла. Заменяем alpha-blend или alpha-to-coverage там где возможно.
Precision qualifiers в GLSL/Metal
// МЕДЛЕННО — highp везде по умолчанию
uniform highp mat4 ModelMatrix;
varying highp vec2 TexCoord;
// БЫСТРО — минимально необходимая точность
uniform highp mat4 ModelMatrix; // матрицы — highp обязательно
varying mediump vec2 TexCoord; // UV-координаты — mediump достаточно
varying lowp vec4 VertexColor; // цвет — lowp
На Mali GPU переход с highp на mediump для texture samplers — от 10 до 25% прирост производительности фрагментного шейдера.
ALU vs Texture Fetch
На большинстве мобильных GPU texture fetch дешевле чем тяжёлые ALU-вычисления (sin, pow, sqrt). Предварительно запечённые lookup таблицы в текстуре быстрее чем вычисление в шейдере:
// Медленно: вычисляем fresnel в шейдере
float fresnel = pow(1.0 - dot(viewDir, normal), 5.0);
// Быстро: lookup texture
float fresnel = texture2D(fresnelLUT, vec2(dot(viewDir, normal), roughness)).r;
Разрешение и Dynamic Resolution Scaling
Рендерить в нативном разрешении iPhone 15 Pro (2556×1179) — избыточно для мобильной игры с интенсивным рендерингом. Стандартная практика — render scale 0.7–0.85 от нативного с последующим upscale.
Unity URP Dynamic Resolution:
ScalableBufferManager.ResizeBuffers(0.75f, 0.75f); // 75% от нативного
Unreal Mobile Super Resolution (MSR) — встроенный temporal upscaler для мобильных платформ с Unreal 5.1+. r.Mobile.TemporalAA 1. Даёт качество близкое к нативному при значительно меньшей GPU-нагрузке.
Adaptive Performance (Samsung Game SDK + Unity): автоматически снижает нагрузку при перегреве:
using UnityEngine.AdaptivePerformance;
var ap = Holder.Instance;
ap.ThermalStatus.ThermalMetrics // текущая температура
ap.PerformanceStatus.PerformanceMetrics // bottleneck info
Кейс: 40 → 58 FPS на Adreno 618
Runner-игра: на Galaxy A52 — 40 FPS. Профилирование через AGI показало: Fragment ALU 87%, fragment bandwidth — перегружен. Три изменения:
- Шейдер воды: заменили
pow(fresnel, 5.0)на LUT-текстуру → -8ms GPU -
highp→mediumpдля всех texture samplers → -4ms GPU - Dynamic resolution 0.80 вместо нативного → -6ms GPU
Итого: с 40 до 58 FPS без изменения визуального стиля. На Pro-устройствах — без изменений, они держали 60 FPS с запасом.
Сроки
Профилирование и анализ рендеринга — 2–3 дня. Оптимизация шейдеров, batching, dynamic resolution — 1–3 недели в зависимости от состояния проекта.







