Текстуры — прожорливое чудовище, пожирающее ваш VRAM
В прошлый раз мы уменьшили вершины вдвое, и модель немного уменьшилась, но не стала молниеносной — потому что настоящий пожиратель размера всё ещё на месте: текстуры. В PBR-модели текстуры обычно занимают 80%+ размера, и именно они раздуваются в VRAM сильнее всего.
Эта статья — лекарство от «жадности текстур к VRAM». Три вопроса: почему PNG/JPG виноваты глазами GPU; как выглядят форматы текстур, родные для GPU, и почему их нельзя использовать напрямую; и как Basis Universal + KTX2 связывают всё воедино.
Напоминание: почему JPG взрывает VRAM
В предыдущей статье мы привели формулу:
Использование VRAM = ширина * высота * 4 байта (RGBA) * 1.333 (с мипмапами)
Текстура 4096x4096, будь она JPG весом 1.5 МБ или PNG весом 8 МБ на диске, превращается в ~87 МБ VRAM. Одна причина: GPU не понимает JPG/PNG.
Семплер текстур GPU понимает только одно: по координате UV прочитать цвет из блока фиксированного размера. Он требует, чтобы текстура была «раскладкой сырых пикселей» в VRAM. Поэтому прежде чем браузер загрузит JPG в GPU, он должен сначала полностью распаковать его в RGBA-пиксели на CPU, а затем запихнуть весь блок в VRAM.
У этого процесса три проблемы:
- Взрыв VRAM: распакованные сырые пиксели огромны. 87 МБ — это не преувеличение, а результат расчёта по формуле.
- Задержка загрузки: перемещение большого блока пикселей из памяти CPU в VRAM GPU — медленная операция, блокирующая первый кадр.
- Нагрузка на CPU: декодирование большого изображения само по себе затратно, особенно на мобильных устройствах.
Развивая метафору «сжатой губки» из прошлого раза: PNG/JPG — это губка, сжатая для удобства перевозки; попадая на GPU, губка впитывает воду и разбухает обратно до полного размера. Скачивание ускорилось; экономия VRAM — ноль.
Форматы текстур, родные для GPU: сжатые в VRAM по своей природе
Раз GPU не принимает предварительно сжатый PNG, можем ли мы сохранить текстуру сжатой даже внутри VRAM? GPU распаковывает отдельный блок пикселей на лету при выборке, практически без затрат.
Именно это делают форматы текстур, родные для GPU. Основные семейства:
| Семейство форматов | Полное название | Основные платформы | Примечания |
|---|---|---|---|
| BC1-7 | Block Compression | Десктоп (ПК, Mac) | Ветеран, блочное сжатие 4x4 пикселей на каждую версию |
| ETC1/2 | Ericsson Texture Compression | Мобильные (старые Android/iOS) | Старый мобильный стандарт |
| ASTC | Adaptive Scalable Texture Compression | Мобильные/VR (новые устройства) | Гибкий, лучшее качество, настраиваемый на блок |
| PVRTC | PowerVR | Старые iOS | Вытесняется ASTC |
Все эти форматы общей чертой: текстуры хранятся сжатыми блоками по 4x4 пикселя, а GPU распаковывает небольшой блок по запросу при выборке — на выходе не один пиксель, а целый блок. Выигрыш в том, что использование VRAM уменьшается на фиксированный коэффициент независимо от содержимого.
Сравнение:
| PNG/JPG (традиционные) | Форматы, родные для GPU | |
|---|---|---|
| Размер на диске | Маленький (особенно JPG) | Средний (блочное сжатие, фиксированный битрейт) |
| Использование VRAM | Большое (распаковано в сырые пиксели) | Маленькое (блочное сжатие, резидентное) |
| Загрузка в GPU | Медленная (декодирование CPU + большой объём передачи) | Быстрая (просто перемещение, без декодирования) |
| Скорость выборки | Быстрая (уже сырые пиксели) | Быстрая (аппаратное декодирование в реальном времени) |
Форматы GPU кажутся идеальным решением. Так почему бы их просто не использовать?
Вот в чём проблема: разные устройства распознают разные форматы
Это главная ловушка форматов текстур GPU — фрагментация.
- Десктопные ПК распознают 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
Ключевые моменты:
- Офлайн-кодирование выполняется один раз, давая компактное промежуточное представление
- Транскодирование в рунтайме очень быстрое (чистые вычисления, несколько миллисекунд), и оно транскодирует блочные форматы — поопиксельная распаковка не нужна
- То, что попадает в VRAM после транскодирования, — это настоящий формат, родный для GPU, поэтому использование VRAM рассчитывается по ставкам блочного сжатия, идентичным форматам 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, вот реальный след текстуры 4096x4096 RGBA при различных вариантах:
| Вариант | Размер на диске | Использование VRAM (с мипмапами) | Скорость загрузки | Кроссплатформенность |
|---|---|---|---|---|
| PNG | ~8 МБ | ~87 МБ | Медленная (требует декодирование) | ✅ |
| JPG | ~1.5 МБ | ~87 МБ | Медленная (требует декодирование) | ✅ |
| WebP | ~2 МБ | ~87 МБ | Медленная (требует декодирование) | ✅ |
| KTX2 (ETC1S) | ~2-3 МБ | ~11-14 МБ | Быстрая | ✅ (транскодирование) |
| KTX2 (UASTC) | ~6-8 МБ | ~22 МБ | Быстрая | ✅ (транскодирование) |
Откуда берутся цифры VRAM: блочное сжатие GPU обычно считается 4bpp (4 бита на пиксель) или 8bpp. 4096x4096 при 4bpp — это около 8 МБ, x1.333 с мипмапами ≈ 11 МБ. UASTC в основном транскодируется в 8bpp, поэтому около 22 МБ.
Суть не в точном числе той или иной строки, а в двух вещах:
- Традиционные форматы (PNG/JPG/WebP) имеют практически идентичное использование VRAM — все распакованные сырые пиксели, 87 МБ. Каким бы маленьким ни был файл на диске, VRAM не экономится.
- KTX2 снижает VRAM до 1/4 — 1/8, и размер на диске тоже конкурентоспособен.
Именно поэтому VR и мобильный веб почти всегда используют KTX2 — сколько текстур по 87 МБ поместится в 2 ГБ VRAM телефона? При 11 МБ помещается семь.
Матрица поддержки платформ: какие GPU распознают какие форматы
Basis избавляет нас от подробностей, но понимание лежащей в основе карты помогает при устранении неполадок. Вот текущая поддержка нативных форматов на основных устройствах:
| Платформа / устройство | BC1-7 | ETC2 | ASTC | PVRTC |
|---|---|---|---|---|
| Десктопный ПК (D3D11/12, Vulkan, WebGPU) | ✅ | ❌ | Частично (новые GPU) | ❌ |
| macOS (Metal) | ✅ (новые модели) | ❌ | ✅ | ❌ |
| Android (основные) | ❌ | ✅ | ✅ | ❌ |
| iOS (A8+) | ❌ | ✅ | ✅ | ✅ (старые устройства) |
| WebGL 2 | Зависит от расширений | ✅ | Частично | ❌ |
| WebGPU | ✅ (десктоп) | ✅ | ✅ (зависит от устройства) | ❌ |
Basis проверяет эти возможности в рунтайме и транскодирует одну и ту же промежуточную кодировку в лучший вариант. Именно поэтому этот слой Basis практически незаменим в вебе — вы не можете предсказать устройство пользователя перед публикацией.
Поток загрузки: сравнение традиционного и форматов GPU
Наконец, зафиксируйте разницу в одной схеме.
Традиционный PNG/JPG:
Файл PNG ──скачивание──> Память CPU ──декодирование CPU (медленно)──> Блок пикселей RGBA ──загрузка (большой объём, медленно)──> VRAM (87 МБ)
KTX2 + Basis:
Файл KTX2 ──скачивание──> Память CPU ──транскодирование на лету (быстро)──> Блочный формат GPU ──загрузка (маленький объём, быстро)──> VRAM (11 МБ)
Второй вариант убирает большой шаг «поопиксельного декодирования CPU», а объём загружаемых данных на порядок меньше. Быстрее первый кадр, меньше VRAM — это и есть ключевая ценность подхода.
Что дальше
Теория завершена; следующая статья — практика. Мы будем использовать toktx и gltf-transform для реального сжатия текстур в KTX2, загрузки их в Three.js / Babylon.js, а также обсудим, как выбирать между ETC1S и UASTC и как настраивать параметры сжатия.