---
title: "Отрисовка 2D-игры в браузере: пакетная отправка спрайтов и переход с WebGPU на WebGL2"
description: "Что такое вызов отрисовки и почему 1000 спрайтов стоит рисовать одним пакетом, как атлас текстур и порядок слоёв влияют на число пакетов, сколько байт уходит в видеокарту за кадр, чем WebGPU отличается от WebGL2, где он работает по данным caniuse на ноябрь 2026 года и как игре перейти на WebGL2, если WebGPU недоступен."
slug: webgpu-i-webgl2-kak-rabotaet-otrisovka-2d-igry
date: 2026-11-04
updated: 2026-11-04
scheduled_at: "2026-11-04T09:00:00+03:00"
rubric: ustrojstvo-2d-igr
tags: ["webgpu", "webgl2", "otrisovka", "profi", "2d-dvizhok"]
reading_time: 9
draft: false
summary: "Скорость отрисовки 2D-игры в браузере чаще упирается не в число спрайтов, а в число вызовов отрисовки: каждая смена текстуры, шейдера или режима смешивания разрывает пакет. Поэтому спрайты собирают в атласы и рисуют сгруппированно по слоям. WebGPU — преемник WebGL с доступом к современным графическим интерфейсам ОС, но по данным caniuse (выпуск от 29.10.2026) полностью его поддерживают браузеры около 85,8% пользователей против 97,13% у WebGL2, поэтому игре нужен переход на WebGL2 при запуске."
---

## Коротко

Браузерная 2D-игра тормозит на отрисовке чаще из-за числа вызовов отрисовки, чем из-за числа спрайтов: каждая смена текстуры, шейдера или режима смешивания начинает новый пакет. Поэтому спрайты складывают в атласы и рисуют сгруппированно, по слоям и по атласам. WebGPU — преемник WebGL, работающий поверх Direct3D 12, Metal и Vulkan, но по данным caniuse на конец октября 2026 года его полностью поддерживают браузеры около 85,8% пользователей, поэтому игре нужен запасной путь через WebGL2.

## Содержание

1. Вызов отрисовки и пакет: что на самом деле стоит дорого
2. Что разрывает пакет: правило подсчёта
3. Сколько данных уходит в видеокарту за кадр
4. WebGPU и WebGL2: чем отличаются и где работают в ноябре 2026
5. Как устроен переход с WebGPU на WebGL2
6. Практический сценарий: 600 монет на экране
7. Когда вам это не подойдёт

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

Дорого стоит не сам спрайт, а каждая отдельная команда видеокарте «нарисуй вот это»: вызов отрисовки. Видеокарта рисует тысячи треугольников за один вызов почти так же быстро, как два, а подготовка каждого вызова (проверка состояния, привязка текстуры и шейдера) ложится на процессор и браузер.

Документация MDN *WebGL best practices* формулирует правило прямо: объединяйте вызовы в меньшее число крупных; если нужно нарисовать 1000 спрайтов, попробуйте сделать это одним вызовом `drawArrays()` или `drawElements()` (MDN, раздел «Batch draw calls», проверено: ноябрь 2026). Там же названа ещё одна техника — атлас текстур: несколько картинок в одной текстуре, потому что для смены текстуры пакет приходится разрывать.

Таблица: Один и тот же кадр с 1000 спрайтов

| Способ | Вызовов отрисовки | Что делает процессор |
|---|---|---|
| Каждый спрайт отдельно | `1000` | 1000 раз готовит состояние и отправляет команду |
| Все спрайты из одного атласа одним пакетом | `1` | Один раз заполняет буфер вершин и отправляет одну команду |
| 4 атласа, спрайты отсортированы по атласу | `4` | Четыре команды |

## Что разрывает пакет: правило подсчёта

Новый пакет начинается каждый раз, когда в порядке рисования меняется хотя бы одно из состояний: текстура (атлас), шейдер (материал), режим смешивания или цель отрисовки. Отсюда простое правило, которое можно проверить на бумаге до запуска игры:

```text
число пакетов = 1 + число смен состояния в порядке рисования

Пример: слой «Фон» (атлас A), слой «Мир» (атласы A и B вперемешку), слой «Интерфейс» (атлас C)

Вперемешку A, B, A, B ... для 200 объектов мира:
    1 (фон) + 200 (мир, смена на каждом объекте) + 1 (интерфейс) ≈ 202 пакета

Внутри слоя «Мир» сначала все из A, потом все из B:
    1 (фон A) + 0 (мир A продолжает пакет фона) + 1 (мир B) + 1 (интерфейс C) = 3 пакета
```

Порядок слоёв при этом менять нельзя: то, что должно быть поверх, рисуется позже. Сортировать по атласу можно только внутри одного слоя и только те объекты, которые не перекрываются полупрозрачными краями. Поэтому практический рецепт такой: один атлас на слой, где это возможно, и как можно меньше слоёв, в которых спрайты из разных атласов перемешаны.

## Сколько данных уходит в видеокарту за кадр

Для типичного спрайтового пакета это десятки килобайт за кадр, и этот объём легко посчитать заранее. Прямоугольный спрайт — это `4` вершины и `2` треугольника (`6` индексов). Если на вершину приходится позиция (`2` числа по `4` байта), координаты в атласе (`2` × `4` байта) и цвет (`4` байта), получается `20` байт:

```text
байт на спрайт  = 4 вершины × 20 байт = 80 байт
1000 спрайтов   = 80 000 байт ≈ 78 КБ на кадр
при 60 кадр/с   = 4 800 000 байт ≈ 4,6 МБ в секунду

индексы (6 штук по 2 байта на спрайт) статичны: их загружают один раз
```

Это наш расчёт для примерного формата вершины; реальные движки упаковывают данные по-своему. Вывод от формата не зависит: при пакетной отправке узкое место смещается с числа команд на объём буфера, а он для 2D-сцены невелик.

## WebGPU и WebGL2: чем отличаются и где работают в ноябре 2026

WebGPU — новый графический интерфейс браузера, который MDN называет преемником WebGL: он работает поверх современных интерфейсов ОС (Direct3D 12, Metal, Vulkan), даёт доступ к вычислениям общего назначения на видеокарте, а для OpenGL, а значит и для WebGL, новых обновлений не планируется (MDN, *WebGPU API*, проверено: ноябрь 2026). WebGPU доступен только на защищённых страницах (HTTPS).

Первым WebGPU по умолчанию включил Chrome 113: о выпуске команда Chrome объявила 6 апреля 2023 года, первая версия работала на ChromeOS, macOS и Windows (Chrome for Developers, *Chrome ships WebGPU*, проверено: ноябрь 2026). По данным MDN, с версии 144 Chrome поддерживает WebGPU и на Linux, но только с видеокартами Intel Gen12 и новее.

Таблица: Поддержка по данным caniuse (выпуск базы 1.0.30001820 от 29.10.2026, проверено: ноябрь 2026)

| Браузер | WebGPU | WebGL2 |
|---|---|---|
| Chrome и Edge на компьютере | Да, с версии `113` | Да (Chrome с `56`, Edge с `79`) |
| Chrome для Android | Да (по MDN — с версии `121`) | Да |
| Safari на macOS | Частично с `26.0`: по умолчанию только на macOS 26 Tahoe и новее | Да, с версии `15` |
| Safari на iPhone и iPad | Да, с версии `26.0` | Да, с версии `15` |
| Firefox на компьютере | Частично с `141`: по умолчанию на Windows и на macOS 26 Tahoe с Apple Silicon | Да, с версии `51` |
| Firefox для Android | Нет (выключен по умолчанию) | Да |
| Общий охват пользователей | `85,8%` полностью + `3,66%` частично ≈ `89,5%` | `97,13%` |

Разница означает простую вещь: игра, которая умеет рисовать только через WebGPU, сегодня не запустится примерно у 10–14% пользователей (в зависимости от того, учитывать ли частичную поддержку), то есть примерно у каждого седьмого–десятого, а без WebGL2 остаются около 3%. Эти цифры меняются с каждым выпуском браузеров, поэтому их нужно перепроверять перед выбором технологии.

## Как устроен переход с WebGPU на WebGL2

Переход устроен как проверка при запуске: если браузер не дал доступ к WebGPU, игра создаёт контекст WebGL2 и рисует через него. Проверка состоит из трёх шагов:

```text
1. есть ли navigator.gpu?                 нет → WebGL2
2. вернул ли requestAdapter() адаптер?     нет (null или ошибка) → WebGL2
3. удалось ли requestDevice()?             нет → WebGL2
иначе → WebGPU

отдельно: страница открыта не по HTTPS → navigator.gpu нет → WebGL2
```

В коде это одна асинхронная функция. Объекты `nav` и `canvas` передаются параметрами, чтобы функцию можно было проверить без браузера:

```js
// Выбор способа отрисовки при запуске игры.
// nav — объект navigator, canvas — элемент canvas.
async function pickRenderer(nav, canvas) {
  if (nav && nav.gpu) {
    try {
      const adapter = await nav.gpu.requestAdapter();
      if (adapter) {
        const device = await adapter.requestDevice();
        return { kind: "webgpu", device: device };
      }
    } catch (err) {
      // ошибка на любом шаге — идём к WebGL2
    }
  }
  const gl = canvas.getContext("webgl2");
  if (gl) return { kind: "webgl2", gl: gl };
  return { kind: "none" }; // показать игроку сообщение, а не пустой экран
}

// Проверка без браузера: поддельные navigator и canvas.
const fakeCanvas = { getContext: function (name) { return name === "webgl2" ? {} : null; } };
const noGpu = {};
const nullAdapter = { gpu: { requestAdapter: async function () { return null; } } };
const goodGpu = { gpu: { requestAdapter: async function () {
  return { requestDevice: async function () { return { label: "dev" }; } };
} } };

(async function () {
  console.log((await pickRenderer(noGpu, fakeCanvas)).kind);       // webgl2
  console.log((await pickRenderer(nullAdapter, fakeCanvas)).kind); // webgl2
  console.log((await pickRenderer(goodGpu, fakeCanvas)).kind);     // webgpu
})();
```

Важно, что после такого выбора вся остальная игра не должна знать, какой путь сработал: правила пакетной отправки из первых разделов одинаково работают и в WebGPU, и в WebGL2.

## Практический сценарий: 600 монет на экране

Задача: на уровне `1280×720` одновременно видны `600` монет, `1` герой, `40` тайлов фона и интерфейс; нужно, чтобы кадр укладывался в `16,67 мс` при `60` кадр/с (`1000 мс ÷ 60`).

1. **Посчитайте пакеты на бумаге.** Если монеты, герой и тайлы лежат в одном атласе, а интерфейс — во втором, по формуле выше выходит `2` пакета; если у монеты свой файл, а между монетами рисуются враги из другого файла, пакетов может быть сотни.
2. **Разложите по слоям:** `Фон`, `Мир` (монеты и герой), `Интерфейс`. Внутри `Мира` рисуйте сначала все спрайты одного атласа, потом другого.
3. **Посчитайте вызовы в коде.** В WebGL2 достаточно обернуть `drawElements` счётчиком на время отладки.
4. **Замерьте время кадра** через `performance.now()` в начале и в конце кадра и сравните со `16,67 мс`.
5. **Проверьте оба пути:** на устройстве с WebGPU и на устройстве без него (например, Firefox для Android) игра должна работать одинаково.

Счётчик вызовов и замер кадра на время отладки:

```js
// Обёртка, которая считает вызовы drawElements за кадр.
function countDrawCalls(gl) {
  const realDraw = gl.drawElements.bind(gl);
  const stats = { calls: 0 };
  gl.drawElements = function (mode, count, type, offset) {
    stats.calls += 1;
    return realDraw(mode, count, type, offset);
  };
  return stats;
}

const BUDGET_MS = 1000 / 60; // 16,67 мс

function measureFrame(stats, drawScene) {
  stats.calls = 0;
  const t0 = performance.now();
  drawScene();
  const ms = performance.now() - t0;
  return { calls: stats.calls, ms: ms, ok: ms <= BUDGET_MS };
}

// Проверка без браузера: поддельный gl и сцена из 600 монет в одном атласе.
const fakeGl = { drawElements: function () {} };
const stats = countDrawCalls(fakeGl);
const coins = [];
for (let i = 0; i < 600; i++) coins.push({ atlas: "A" });
function drawScene() {
  let current = null;
  for (const c of coins) {
    if (c.atlas !== current) { if (current) fakeGl.drawElements(); current = c.atlas; }
  }
  fakeGl.drawElements(); // последний пакет
}
const r = measureFrame(stats, drawScene);
console.log("вызовов:", r.calls, "бюджет:", BUDGET_MS.toFixed(2), "мс"); // вызовов: 1 бюджет: 16.67 мс
```

Время, измеренное `performance.now()` вокруг вызовов WebGL, показывает работу процессора на подготовку кадра, а не время работы видеокарты; для первой проверки «не слишком ли много пакетов» этого достаточно.

### Готовый чек-лист «быстрой отрисовки»

| № | Правило | Как проверить |
|---|---|---|
| 1 | Один атлас на слой, где это возможно | Посчитать смены атласа в порядке рисования по формуле `1 + число смен` |
| 2 | Минимум слоёв со спрайтами из разных атласов вперемешку | Выписать порядок рисования слоя и отметить смены атласа |
| 3 | Не рисовать объекты за пределами экрана | Сравнить число вызовов на пустом и заполненном участке уровня |
| 4 | Бюджет кадра — `16,67 мс` при `60` кадр/с | `performance.now()` в начале и в конце кадра |
| 5 | Игра проверена и на WebGPU, и на WebGL2 | Два устройства: одно с WebGPU, одно без |

## Когда вам это не подойдёт

Правила пакетной отправки и перехода на WebGL2 закрывают отрисовку типичной 2D-сцены, но не всё:

- **Узкое место не в отрисовке.** Если замер показывает, что время уходит на логику (например, проверку каждого объекта с каждым), атласы не помогут — сначала разберитесь с проверкой столкновений.
- **Нужны вычисления на видеокарте.** Частицы или симуляции на вычислительных шейдерах есть только в WebGPU; на WebGL2 их придётся делать иначе или отказаться от них на части устройств.
- **Сотни полупрозрачных перекрывающихся спрайтов из разных атласов.** Сортировать их по атласу нельзя без нарушения порядка наложения, поэтому пакетов будет много — такой эффект лучше собрать в один атлас.
- **Простая игра на Canvas2D.** Если на экране десятки спрайтов, а не сотни, обычный `CanvasRenderingContext2D` справится, и переход на WebGL2 или WebGPU не окупит сложность.

Мы разрабатываем **J2DRU** — браузерный 2D игровой движок, который в браузере без WebGPU работает через WebGL2; он в разработке и публично пока не открыт, гарантий частоты кадров мы не даём, а заявку можно оставить на [странице раннего доступа](/early-access?utm_source=blog&utm_medium=article&utm_campaign=webgpu-i-webgl2-kak-rabotaet-otrisovka-2d-igry) (подача заявки не гарантирует приглашение).

**Источники (проверено: ноябрь 2026):** MDN, *WebGL best practices* (раздел «Batch draw calls», атласы текстур); MDN, *WebGPU API* (преемник WebGL, Direct3D 12 / Metal / Vulkan, только HTTPS); MDN Browser Compat Data, `api.GPU` (Chrome 144 на Linux, Chrome для Android с 121, Firefox 141 и 145); Chrome for Developers, *Chrome ships WebGPU*, 06.04.2023; caniuse.com, таблицы «WebGPU» и «WebGL 2.0», выпуск базы 1.0.30001820 от 29.10.2026. Расчёт байт на спрайт и число пакетов в примерах — наши.

**Читайте также:**
- [Пиксель-арт в браузере без размытия](/blog/piksel-art-v-brauzere-bez-razmytiya)
- [Игровой цикл и физика столкновений AABB](/blog/igrovoj-cikl-i-fizika-stolknovenij-aabb)
- [Тайловые сетки и устройство 2D-миров](/blog/tajlovye-setki-i-ustrojstvo-2d-mirov)
