Текстуры — прожорливые пожиратели видеопамяти
В прошлой статье мы сократили количество вершин вдвое, модель стала меньше, но не превратилась в молнию — настоящий гигант объёма всё ещё здесь: текстуры. В PBR-модели текстуры обычно занимают более 80% объёма, и в видеопамяти они разбухают сильнее всего.
Эта статья посвящена проблеме «прожорливых пожирателей видеопамяти» среди текстур. Расскажу о трёх вещах: почему PNG/JPG грешны перед GPU; как выглядят родные форматы текстур GPU и почему их нельзя использовать напрямую; а также как связка Basis Universal + KTX2 решает все три проблемы.
Повторение: почему JPG взрывает видеопамять
В прошлой статье была формула:
Занимаемая видеопамять = ширина × высота × 4 байта (RGBA) × 1,333 (с мип-уровнями)
Текстура 4096×4096, будь то 1,5 МБ JPG или 8 МБ PNG на диске, в видеопамяти занимает ~87 МБ. Причина одна: GPU не понимает JPG/PNG.
Текстурный сэмплер GPU знает только одно: по UV-координатам прочитать цвет из фиксированного блока пикселей. Он требует, чтобы текстура в видеопамяти была «разложена в исходные пиксели». Поэтому браузер, прежде чем загрузить JPG в GPU, должен сначала полностью распаковать его на CPU в RGBA-пиксели, а затем загрузить весь блок в видеопамять.
У этого процесса три проблемы:
- Взрыв видеопамяти: распакованные исходные пиксели занимают огромное количество — 87 МБ не преувеличение, это расчёт.
- Блокировка загрузки: перемещение большого блока пикселей из CPU в GPU — медленная операция, задерживающая отрисовку первого кадра.
- Нагрузка на CPU при распаковке: распаковка больших изображений сама по себе требует времени, особенно на мобильных устройствах.
Продолжая метафору из прошлой статьи: PNG/JPG — это сжатая губка, удобная для транспортировки; как только она попадает в GPU, губка впитывает воду и разбухает до исходного размера. Загрузка ускорилась, но видеопамять не сэкономилась ни на байт.
Родные форматы текстур GPU: сжатые прямо в видеопамяти
Раз GPU не принимает сжатые PNG, может ли текстура оставаться сжатой и в видеопамяти? GPU при семплировании декодирует отдельные блоки пикселей в реальном времени, почти без накладных расходов.
Это и есть родные форматы текстур GPU (GPU-native texture format). Основные семейства:
| Семейство | Полное название | Основные платформы | Особенности |
|---|---|---|---|
| BC1-7 | Block Compression | Десктопы (PC, Mac) | Классика, сжатие блоков 4×4 |
| ETC1/2 | Ericsson Texture Compression | Мобильные (Android/iOS старые) | Старый стандарт для мобильных |
| ASTC | Adaptive Scalable Texture Compression | Мобильные/VR (новые устройства) | Гибкий, лучшее качество, настраиваемый |
| PVRTC | PowerVR | Старые iOS | Постепенно заменяется ASTC |
Общая черта: текстуры сжимаются блоками 4×4 пикселя, GPU при семплировании декодирует этот блок по требованию, выдавая не один пиксель, а целый блок. Преимущество — занимаемая видеопамять уменьшается с фиксированным коэффициентом, независимо от содержимого.
Сравнение:
| PNG/JPG (традиционные) | Родные форматы GPU | |
|---|---|---|
| Размер на диске | Маленький (JPG особенно) | Средний (блочное сжатие, фикс. битрейт) |
| Занимаемая видеопамять | Большая (распакованные в исходные пиксели) | Маленькая (блочное сжатие, постоянно) |
| Загрузка в GPU | Медленная (распаковка CPU + большая передача) | Быстрая (прямая загрузка, без распаковки) |
| Скорость семплирования | Быстрая (уже исходные пиксели) | Быстрая (аппаратное декодирование) |
Кажется, формат GPU — идеальное решение. Почему же его нельзя использовать напрямую?
Проблема: разные устройства поддерживают разные форматы
Вот главная проблема форматов текстур GPU — фрагментация.
- Десктопные PC поддерживают BC1-7, но не ASTC
- Android-смартфоны поддерживают ETC2/ASTC, большинство не поддерживают BC
- iOS (начиная с A7) поддерживает ASTC, старые модели — PVRTC
- WebGPU/WebGL используют те же аппаратные возможности устройства
Если вы хотите, чтобы текстура «существовала в родном формате GPU на всех устройствах», вам нужно подготовить отдельную версию для каждой платформы. Продукт, который выходит на десктоп + Android + iOS, требует трёх версий: BC + ETC2/ASTC. Размер пакета утраивается, объём работы утраивается.
Хуже того, в вебе вы не знаете, какое устройство у пользователя. Сгенерировать заранее все форматы нереально, а обнаружение во время выполнения слишком медленное.
Basis Universal: одно кодирование, везде транскодирование
Basis Universal (сокращённо Basis) был создан именно для решения этой фрагментации. Его идея одной фразой:
Сначала закодировать текстуру в «промежуточный формат», а затем во время выполнения, в зависимости от возможностей GPU устройства, транскодировать (транскодировать) в соответствующий родной формат.
Процесс транскодирования (схема):
Исходная текстура (PNG/JPG)
│ Однократное офлайн-кодирование (медленное, делается один раз)
▼
Промежуточный формат Basis (ETC1S или UASTC)
│ Упаковка в контейнер KTX2
▼
Публикация в веб ──┬── Десктоп GPU ──→ Транскодирование на лету → BC1/3/7
├── Android ──→ Транскодирование на лету → ETC2
└── iOS/VR ──→ Транскодирование на лету → ASTC
Ключевые моменты:
- Офлайн-кодирование делается один раз, получается компактное промежуточное представление
- Транскодирование на лету очень быстрое (чистые вычисления, миллисекунды), и транскодируются блочные форматы, не требуется распаковка каждого пикселя
- После транскодирования в видеопамять поступает настоящий родной формат GPU, занимаемая память считается по блочному сжатию, как у родного формата GPU
Basis предлагает два режима промежуточного кодирования, подробнее в следующей статье, а пока запомните названия:
- ETC1S: очень высокая степень сжатия, подходит для диффузных / альбедо карт и т.д.
- UASTC: более высокое качество, подходит для нормалей и других карт, чувствительных к точности
KTX2: стандартный контейнер для текстур GPU
Остаётся ещё одна инженерная проблема: куда поместить закодированные данные Basis, как их маркировать и как связать с glTF? Ответ: KTX2.
KTX2 (Khronos Texture 2) — это не очередной формат изображения, а контейнерный формат — как .zip, которому всё равно, что внутри: документы или картинки. KTX2 отвечает за упаковку данных текстур GPU (включая Basis-кодированные) в стандартную структуру и добавляет метаинформацию (формат, уровни мип-карт, цветовое пространство и т.д.).
В glTF KTX2 подключается через расширение KHR_texture_basisu: текстура больше не PNG, а файл KTX2, внутри которого Basis-кодирование. При загрузке движок определяет возможности устройства и транскодирует в соответствующий BC/ETC/ASTC.
Давайте разграничим, чтобы не путать:
| Имя | Роль | Аналогия |
|---|---|---|
| Basis Universal | Схема кодирования (как сжать текстуру в промежуточный формат) | «Алгоритм сжатия» |
| KTX2 | Контейнерный формат (как упаковать закодированные данные) | «Коробка» |
| KHR_texture_basisu | Расширение glTF (говорит движку, что это текстура Basis) | «Этикетка» |
Файл KTX2 может содержать внутри Basis-кодирование (кроссплатформенное) или какой-то родной формат (например, напрямую BC7). В вебе в 99% случаев используется Basis, потому что нам нужно «одно кодирование — везде транскодирование».
Пример VRAM: сравнение для текстуры 4096
Сложим предыдущую формулу и форматы GPU, чтобы увидеть реальное использование 4096×4096 RGBA-текстуры в разных подходах:
| Схема | Размер на диске | Занимаемая видеопамять (с мип-уровнями) | Скорость загрузки | Кроссплатформенность |
|---|---|---|---|---|
| PNG | ~8 MB | ~87 MB | Медленно (требует распаковки) | ✅ |
| JPG | ~1.5 MB | ~87 MB | Медленно (требует распаковки) | ✅ |
| WebP | ~2 MB | ~87 MB | Медленно (требует распаковки) | ✅ |
| KTX2 (ETC1S) | ~2-3 MB | ~11-14 MB | Быстро | ✅ (транскодирование) |
| KTX2 (UASTC) | ~6-8 MB | ~22 MB | Быстро | ✅ (транскодирование) |
Как получаются цифры: блочное сжатие GPU обычно считается в 4 бита на пиксель (4bpp) или 8bpp. Для 4096×4096 при 4bpp это около 8 MB, с мип-уровнями умножаем на 1,333 ≈ 11 MB. UASTC чаще всего транскодируется в 8bpp, поэтому около 22 MB.
Важны не точные цифры в строке, а два вывода:
- Традиционные форматы (PNG/JPG/WebP) занимают почти одинаково видеопамяти — все распакованные исходные пиксели, 87 MB. На диске мелко, но видеопамять не экономится.
- KTX2 снижает видеопамять в 4–8 раз, при этом размер на диске не проигрывает.
Именно поэтому в VR, мобильном вебе почти обязательно используют KTX2 — сколько текстур по 87 MB поместится в 2 ГБ видеопамяти на телефоне? А по 11 MB — 7 штук.
Матрица поддержки платформ: какие GPU что поддерживают
Хотя Basis скрывает от нас детали, понимание базовой карты помогает в отладке. Вот поддержка родных форматов для современных устройств:
| Платформа / устройство | BC1-7 | ETC2 | ASTC | PVRTC |
|---|---|---|---|---|
| Десктоп PC (D3D11/12, Vulkan, WebGPU) | ✅ | ❌ | Частично (новые GPU) | ❌ |
| macOS (Metal) | ✅ (новые) | ❌ | ✅ | ❌ |
| Android (основные) | ❌ | ✅ | ✅ | ❌ |
| iOS (A8+) | ❌ | ✅ | ✅ | ✅ (старые) |
| WebGL 2 | Зависит от расширения | ✅ | Частично | ❌ |
| WebGPU | ✅ (десктоп) | ✅ | ✅ (в зависимости от устройства) | ❌ |
Basis во время выполнения определяет эти возможности и транскодирует одно и то же промежуточное кодирование в наиболее подходящий формат. Именно поэтому слой Basis на вебе почти незаменим — вы не можете заранее узнать устройство пользователя.
Сравнение процессов загрузки: традиционный vs формат GPU
В завершение закрепим разницу блок-схемой.
Традиционный PNG/JPG:
PNG-файл ──загрузка──> Память CPU ──CPU распаковка (медленно)──> Блок RGBA-пикселей ──загрузка (большой, медленно)──> Видеопамять (87 MB)
KTX2 + Basis:
KTX2-файл ──загрузка──> Память CPU ──транскодирование на лету (быстро)──> Блочный формат GPU ──загрузка (маленький, быстро)──> Видеопамять (11 MB)
Второй вариант избавляется от крупного этапа «распаковка каждого пикселя CPU», а объём передаваемых данных меньше на порядок. Быстрый первый кадр, экономия видеопамяти — вот ключевая ценность этого подхода.
Что дальше
Теория закончена. В следующей статье приступим к практике. Используя toktx, gltf-transform, мы реально сожмём текстуру в KTX2, загрузим её в Three.js / Babylon.js, а также обсудим, как выбирать между ETC1S и UASTC, как настраивать параметры сжатия.