Оптимизация графики: draw calls, batching, LOD
Красивая сцена бесполезна, если тормозит: оптимизация графики — это борьба с лишними командами и лишней геометрией.
Draw call — команда процессора видеокарте нарисовать порцию геометрии; их количество — одно из главных узких мест производительности.
Зачем это знать
Можно иметь мощный GPU и всё равно получить 20 FPS — если CPU захлёбывается, отправляя тысячи мелких команд. Оптимизация графики экономит и время CPU (меньше команд), и время GPU (меньше пикселей и вершин). Это финальный, но решающий навык.
Проблема draw calls
Каждый draw call имеет фиксированные накладные расходы на стороне CPU. Тысяча отдельных объектов = тысяча команд, и CPU не успевает их готовить. Решение — batching: объединить много объектов в один вызов.
cpu_cost_per_call = 0.05 # мс на один draw call (условно)
for objects in [100, 1000, 5000]:
no_batch = objects * cpu_cost_per_call
batched = (objects / 100) * cpu_cost_per_call # по 100 в батч
print(f"{objects} объектов: без батчинга {no_batch:.1f} мс, с батчингом {batched:.2f} мс")
Вывод:
100 объектов: без батчинга 5.0 мс, с батчингом 0.05 мс 1000 объектов: без батчинга 50.0 мс, с батчингом 0.50 мс 5000 объектов: без батчинга 250.0 мс, с батчингом 2.50 мс
5000 объектов по отдельности — 250 мс на одни только команды (катастрофа). Объединив их в батчи по 100, тратим 2.5 мс. Бюджет кадра — напомним, всего ~16.7 мс.
LOD: уровни детализации
Далёкий объект занимает несколько пикселей — незачем рисовать его миллионом полигонов. LOD подменяет дальние модели упрощёнными. Чем дальше — тем грубее меш.
def choose_lod(distance):
if distance < 10: return ("LOD0", 10000) # вблизи: детально
if distance < 50: return ("LOD1", 2000)
if distance < 200: return ("LOD2", 400)
return ("LOD3", 50) # вдали: грубо
for d in [5, 30, 120, 500]:
lod, tris = choose_lod(d)
print(f"дистанция {d}: {lod}, {tris} треугольников")
Вывод:
дистанция 5: LOD0, 10000 треугольников дистанция 30: LOD1, 2000 треугольников дистанция 120: LOD2, 400 треугольников дистанция 500: LOD3, 50 треугольников
На экране разница незаметна (объект далеко), а нагрузка падает в сотни раз.
Culling: не рисовать невидимое
| Вид отсечения | Что отбрасывает |
| frustum culling | объекты вне поля зрения камеры |
| backface culling | грани, повёрнутые от камеры |
| occlusion culling | объекты, полностью закрытые другими |
Как работает под капотом
Современные движки используют instancing (нарисовать тысячу одинаковых деревьев одним вызовом с разными матрицами), атласы текстур (много текстур в одной, чтобы не переключать состояние) и GPU-driven рендеринг (сам GPU решает, что рисовать). Профилировщик показывает, во что упёрся кадр — в CPU (много draw calls) или GPU (тяжёлые шейдеры, overdraw).
Частые ошибки
- Тысячи отдельных объектов без батчинга/instancing — CPU становится узким местом.
- Рисовать дальние объекты в полной детализации — впустую тратится GPU.
- Overdraw: много перекрывающихся прозрачных слоёв заставляют шейдер считать одни пиксели многократно.
Итоги
- Draw calls дороги на стороне CPU; batching и instancing их сокращают.
- LOD снижает детализацию дальних объектов без видимой потери качества.
- Culling (frustum, backface, occlusion) не рисует невидимое.
- Профилировщик показывает, упёрся кадр в CPU или GPU.