Понимание render loop: CPU vs GPU bottleneck

В основе любого интерактивного 3D-приложения лежит непрерывный цикл отрисовки — render loop. Этот цикл отвечает за обновление состояния сцены, выполнение вычислений на стороне процессора и передачу графических команд видеокарте. В библиотеке Babylon.js данный механизм построен вокруг объекта Engine, который управляет жизненным циклом кадров.

Типичная структура render loop выглядит следующим образом:

const engine = new BABYLON.Engine(canvas, true);
const scene = createScene();

engine.runRenderLoop(function () {
    scene.render();
});

Каждая итерация цикла соответствует одному кадру. Во время выполнения:

  1. Выполняются обновления сцены.
  2. Обрабатываются анимации.
  3. Рассчитывается положение камеры.
  4. Подготавливаются команды рендеринга.
  5. Команды отправляются в GPU.
  6. GPU выполняет шейдеры и формирует изображение.

Частота выполнения цикла обычно синхронизируется с частотой обновления дисплея (чаще всего 60 FPS) через requestAnimationFrame.


Этапы render loop

Render loop можно условно разделить на несколько последовательных стадий.

1. Обновление логики сцены

На этом этапе CPU выполняет вычисления:

  • обработка пользовательского ввода
  • обновление физических симуляций
  • пересчёт трансформаций объектов
  • обновление анимаций
  • вычисление матриц

В Babylon.js эти операции происходят внутри scene.render().

Внутренне этот процесс включает:

  • _evaluateActiveMeshes
  • _updateTransformMatrix
  • _animate
  • _processSubCameras

Каждая из этих функций выполняется на стороне CPU.


2. Подготовка рендер-команд

CPU формирует список объектов, которые должны быть отрисованы.

Этот процесс включает:

  • frustum culling — исключение объектов вне камеры
  • sorting — сортировка прозрачных и непрозрачных мешей
  • подготовку материалов
  • привязку текстур
  • установку параметров шейдеров

После подготовки данные передаются через WebGL API.

Пример типичного вызова WebGL:

gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0);

CPU лишь отправляет команды, а реальное выполнение происходит на GPU.


3. Выполнение шейдеров на GPU

GPU выполняет две основные программы:

Vertex Shader

  • трансформирует вершины
  • применяет матрицы
  • рассчитывает нормали

Fragment Shader

  • рассчитывает цвет пикселя
  • применяет освещение
  • накладывает текстуры
  • выполняет постобработку

GPU обрабатывает тысячи вершин и пикселей параллельно, что делает его значительно быстрее CPU для графических задач.


Понятие bottleneck

Bottleneck — узкое место в системе, ограничивающее производительность.

В графическом приложении возможны два основных типа:

  1. CPU bottleneck
  2. GPU bottleneck

Render loop может ограничиваться одним из них.


CPU Bottleneck

CPU bottleneck возникает, когда процессор не успевает подготовить данные для GPU.

В этом случае GPU простаивает, ожидая новые команды.

Основные признаки:

  • низкая загрузка GPU
  • высокий процент использования CPU
  • FPS падает при увеличении количества объектов сцены

Причины CPU Bottleneck

Большое количество мешей

Каждый Mesh требует:

  • вычисления трансформации
  • проверки на видимость
  • подготовки рендер-команды

Сцена с тысячами объектов создает значительную нагрузку на CPU.

Пример неэффективной сцены:

for (let i = 0; i < 5000; i++) {
    BABYLON.MeshBuilder.CreateBox("box" + i, {}, scene);
}

Каждый объект обрабатывается отдельно.


Частые изменения геометрии

Изменение вершин или буферов заставляет систему повторно отправлять данные на GPU.

mesh.updateVerticesData(
    BABYLON.VertexBuffer.PositionKind,
    newVertices
);

Такие операции дорогие по времени.


Сложная логика JavaScript

Сложные вычисления внутри render loop также могут создавать узкое место.

Пример:

engine.runRenderLoop(() => {

    heavyPhysicsSimulation();

    scene.render();

});

Если симуляция занимает 10–12 мс, FPS уже не сможет достичь 60.


Физические движки

Подключение физики (например Ammo.js или Cannon.js) увеличивает нагрузку на CPU, так как симуляция выполняется именно там.


GPU Bottleneck

GPU bottleneck возникает, когда видеокарта не успевает обработать команды рендеринга.

В этом случае CPU завершает работу быстрее и ожидает завершения GPU.

Признаки:

  • высокая загрузка GPU
  • увеличение времени рендеринга при росте разрешения
  • сложные материалы снижают FPS

Причины GPU Bottleneck

Высокая полигональность

Чем больше треугольников в сцене, тем больше работы для vertex shader.

Например:

  • 100k треугольников — умеренная нагрузка
  • 1M+ треугольников — высокая нагрузка

Babylon.js передает все вершины GPU на обработку.


Сложные материалы

Материалы могут включать:

  • PBR освещение
  • несколько текстур
  • карты нормалей
  • карты отражений
  • прозрачность

Пример материала:

const material = new BABYLON.PBRMaterial("pbr", scene);
material.albedoTexture = new BABYLON.Texture("albedo.jpg");
material.normalTexture = new BABYLON.Texture("normal.jpg");
material.metallicTexture = new BABYLON.Texture("metallic.jpg");

Каждая текстура увеличивает нагрузку на fragment shader.


Большое количество источников света

Каждый источник света добавляет дополнительные вычисления в шейдерах.

Особенно дорогие:

  • PointLight
  • SpotLight

Высокое разрешение экрана

Количество пикселей напрямую влияет на GPU.

Разрешения:

Разрешение Пикселей
1280×720 ~0.9 млн
1920×1080 ~2.1 млн
4K ~8.3 млн

Fragment shader выполняется для каждого пикселя, поэтому рост разрешения увеличивает нагрузку почти линейно.


Как определить тип bottleneck

Babylon.js предоставляет инструменты профилирования.

Scene Instrumentation

const instrumentation = new BABYLON.SceneInstrumentation(scene);

instrumentation.captureFrameTime = true;
instrumentation.captureRenderTime = true;

Доступные показатели:

  • frameTimeCounter
  • drawCallsCounter
  • activeMeshesCounter

EngineInstrumentation

Позволяет измерять загрузку GPU.

const engineInstrumentation = new BABYLON.EngineInstrumentation(engine);

engineInstrumentation.captureGPUFrameTime = true;

Это помогает понять:

  • сколько времени GPU тратит на кадр
  • где возникают задержки

GPU Frame Time

Если:

  • GPU frame time > CPU frame time — система GPU bound.

Если наоборот:

  • CPU frame time > GPU frame time — система CPU bound.

Оптимизация CPU Bottleneck

Инстансирование

Babylon.js поддерживает GPU instancing, позволяющее рисовать множество одинаковых объектов одной командой.

const box = BABYLON.MeshBuilder.CreateBox("box");

for (let i = 0; i < 1000; i++) {
    box.createInstance("box" + i);
}

Это резко снижает количество draw calls.


Merge Meshes

Объединение нескольких мешей в один уменьшает количество объектов сцены.

BABYLON.Mesh.MergeMeshes(meshArray);

CPU обрабатывает меньше элементов.


Thin Instances

Для огромных сцен используется thinInstances.

mesh.thinInstanceAdd(matrix);

Этот механизм хранит только матрицы трансформации.


Ограничение логики в render loop

Дорогие вычисления лучше выполнять:

  • реже
  • в Web Workers
  • заранее

Оптимизация GPU Bottleneck

Упрощение материалов

PBR материалы можно заменить на стандартные:

new BABYLON.StandardMaterial("mat", scene);

Они требуют меньше вычислений.


Уменьшение количества источников света

Babylon.js поддерживает ограничение числа источников света:

scene.maxSimultaneousLights = 4;

LOD (Level of Detail)

LOD автоматически снижает детализацию удалённых объектов.

mesh.addLODLevel(50, lowPolyMesh);

GPU обрабатывает менее сложную геометрию.


Снижение разрешения рендера

Можно уменьшить внутреннее разрешение:

engine.setHardwareScalingLevel(1.5);

GPU будет обрабатывать меньше пикселей.


Draw Calls как ключевой показатель

Draw call — команда отправки объекта на GPU.

Каждый draw call имеет стоимость на CPU.

Количество draw calls можно посмотреть:

scene.getEngine().getDrawCalls();

Желательные значения:

Тип приложения Draw Calls
мобильное < 100
веб-игра < 500
сложная сцена < 2000

Pipeline кадра в Babylon.js

Полный цикл одного кадра включает:

  1. JavaScript логика
  2. обновление сцены
  3. подготовку мешей
  4. отправку draw calls
  5. выполнение vertex shader
  6. rasterization
  7. fragment shader
  8. постобработку
  9. вывод на экран

Каждый этап может стать ограничением производительности.


Баланс CPU и GPU

Оптимальная сцена стремится к равномерной загрузке:

  • CPU не простаивает
  • GPU не перегружен
  • render loop стабильно укладывается в 16.6 ms (для 60 FPS)

При превышении этого времени частота кадров падает.

Понимание того, где именно возникает bottleneck — фундаментальный навык оптимизации 3D-приложений на Babylon.js.