В основе любого интерактивного 3D-приложения лежит непрерывный цикл
отрисовки — render loop. Этот цикл отвечает за
обновление состояния сцены, выполнение вычислений на стороне процессора
и передачу графических команд видеокарте. В библиотеке
Babylon.js данный механизм построен вокруг объекта
Engine, который управляет жизненным циклом кадров.
Типичная структура render loop выглядит следующим образом:
const engine = new BABYLON.Engine(canvas, true);
const scene = createScene();
engine.runRenderLoop(function () {
scene.render();
});
Каждая итерация цикла соответствует одному кадру. Во время выполнения:
Частота выполнения цикла обычно синхронизируется с частотой
обновления дисплея (чаще всего 60 FPS) через
requestAnimationFrame.
Render loop можно условно разделить на несколько последовательных стадий.
На этом этапе CPU выполняет вычисления:
В Babylon.js эти операции происходят внутри
scene.render().
Внутренне этот процесс включает:
_evaluateActiveMeshes_updateTransformMatrix_animate_processSubCamerasКаждая из этих функций выполняется на стороне CPU.
CPU формирует список объектов, которые должны быть отрисованы.
Этот процесс включает:
После подготовки данные передаются через WebGL API.
Пример типичного вызова WebGL:
gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0);
CPU лишь отправляет команды, а реальное выполнение происходит на GPU.
GPU выполняет две основные программы:
Vertex Shader
Fragment Shader
GPU обрабатывает тысячи вершин и пикселей параллельно, что делает его значительно быстрее CPU для графических задач.
Bottleneck — узкое место в системе, ограничивающее производительность.
В графическом приложении возможны два основных типа:
Render loop может ограничиваться одним из них.
CPU bottleneck возникает, когда процессор не успевает подготовить данные для GPU.
В этом случае GPU простаивает, ожидая новые команды.
Основные признаки:
Каждый Mesh требует:
Сцена с тысячами объектов создает значительную нагрузку на CPU.
Пример неэффективной сцены:
for (let i = 0; i < 5000; i++) {
BABYLON.MeshBuilder.CreateBox("box" + i, {}, scene);
}
Каждый объект обрабатывается отдельно.
Изменение вершин или буферов заставляет систему повторно отправлять данные на GPU.
mesh.updateVerticesData(
BABYLON.VertexBuffer.PositionKind,
newVertices
);
Такие операции дорогие по времени.
Сложные вычисления внутри render loop также могут создавать узкое место.
Пример:
engine.runRenderLoop(() => {
heavyPhysicsSimulation();
scene.render();
});
Если симуляция занимает 10–12 мс, FPS уже не сможет достичь 60.
Подключение физики (например Ammo.js или Cannon.js) увеличивает нагрузку на CPU, так как симуляция выполняется именно там.
GPU bottleneck возникает, когда видеокарта не успевает обработать команды рендеринга.
В этом случае CPU завершает работу быстрее и ожидает завершения GPU.
Признаки:
Чем больше треугольников в сцене, тем больше работы для vertex shader.
Например:
Babylon.js передает все вершины GPU на обработку.
Материалы могут включать:
Пример материала:
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.
Каждый источник света добавляет дополнительные вычисления в шейдерах.
Особенно дорогие:
PointLightSpotLightКоличество пикселей напрямую влияет на GPU.
Разрешения:
| Разрешение | Пикселей |
|---|---|
| 1280×720 | ~0.9 млн |
| 1920×1080 | ~2.1 млн |
| 4K | ~8.3 млн |
Fragment shader выполняется для каждого пикселя, поэтому рост разрешения увеличивает нагрузку почти линейно.
Babylon.js предоставляет инструменты профилирования.
const instrumentation = new BABYLON.SceneInstrumentation(scene);
instrumentation.captureFrameTime = true;
instrumentation.captureRenderTime = true;
Доступные показатели:
frameTimeCounterdrawCallsCounteractiveMeshesCounterПозволяет измерять загрузку GPU.
const engineInstrumentation = new BABYLON.EngineInstrumentation(engine);
engineInstrumentation.captureGPUFrameTime = true;
Это помогает понять:
Если:
Если наоборот:
Babylon.js поддерживает GPU instancing, позволяющее рисовать множество одинаковых объектов одной командой.
const box = BABYLON.MeshBuilder.CreateBox("box");
for (let i = 0; i < 1000; i++) {
box.createInstance("box" + i);
}
Это резко снижает количество draw calls.
Объединение нескольких мешей в один уменьшает количество объектов сцены.
BABYLON.Mesh.MergeMeshes(meshArray);
CPU обрабатывает меньше элементов.
Для огромных сцен используется thinInstances.
mesh.thinInstanceAdd(matrix);
Этот механизм хранит только матрицы трансформации.
Дорогие вычисления лучше выполнять:
PBR материалы можно заменить на стандартные:
new BABYLON.StandardMaterial("mat", scene);
Они требуют меньше вычислений.
Babylon.js поддерживает ограничение числа источников света:
scene.maxSimultaneousLights = 4;
LOD автоматически снижает детализацию удалённых объектов.
mesh.addLODLevel(50, lowPolyMesh);
GPU обрабатывает менее сложную геометрию.
Можно уменьшить внутреннее разрешение:
engine.setHardwareScalingLevel(1.5);
GPU будет обрабатывать меньше пикселей.
Draw call — команда отправки объекта на GPU.
Каждый draw call имеет стоимость на CPU.
Количество draw calls можно посмотреть:
scene.getEngine().getDrawCalls();
Желательные значения:
| Тип приложения | Draw Calls |
|---|---|
| мобильное | < 100 |
| веб-игра | < 500 |
| сложная сцена | < 2000 |
Полный цикл одного кадра включает:
Каждый этап может стать ограничением производительности.
Оптимальная сцена стремится к равномерной загрузке:
При превышении этого времени частота кадров падает.
Понимание того, где именно возникает bottleneck — фундаментальный навык оптимизации 3D-приложений на Babylon.js.