Оптимизация графики: 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.
Проверьте себя
1. Что такое draw call и почему его число важно?
AЦвет пикселя
BКоманда CPU видеокарте нарисовать геометрию; их избыток перегружает CPU
CТип текстуры
DУровень mipmap
2. Что делает LOD (уровни детализации)?
AПовышает разрешение текстур
BПодменяет дальние объекты упрощёнными моделями
CУдаляет тени
DСортирует прозрачность
3. Что отбрасывает frustum culling?
AГрани, повёрнутые от камеры
BОбъекты, находящиеся вне поля зрения камеры
CПрозрачные объекты
DДалёкие текстуры