Kris

Текстуры — прожорливые пожиратели видеопамяти

3D-сжатиеСжатие текстурWebGLWebGPU

В прошлой статье мы сократили количество вершин вдвое, модель стала меньше, но не превратилась в молнию — настоящий гигант объёма всё ещё здесь: текстуры. В 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-пиксели, а затем загрузить весь блок в видеопамять.

У этого процесса три проблемы:

  1. Взрыв видеопамяти: распакованные исходные пиксели занимают огромное количество — 87 МБ не преувеличение, это расчёт.
  2. Блокировка загрузки: перемещение большого блока пикселей из CPU в GPU — медленная операция, задерживающая отрисовку первого кадра.
  3. Нагрузка на CPU при распаковке: распаковка больших изображений сама по себе требует времени, особенно на мобильных устройствах.

Продолжая метафору из прошлой статьи: PNG/JPG — это сжатая губка, удобная для транспортировки; как только она попадает в GPU, губка впитывает воду и разбухает до исходного размера. Загрузка ускорилась, но видеопамять не сэкономилась ни на байт.

Родные форматы текстур GPU: сжатые прямо в видеопамяти

Раз GPU не принимает сжатые PNG, может ли текстура оставаться сжатой и в видеопамяти? GPU при семплировании декодирует отдельные блоки пикселей в реальном времени, почти без накладных расходов.

Это и есть родные форматы текстур GPU (GPU-native texture format). Основные семейства:

СемействоПолное названиеОсновные платформыОсобенности
BC1-7Block CompressionДесктопы (PC, Mac)Классика, сжатие блоков 4×4
ETC1/2Ericsson Texture CompressionМобильные (Android/iOS старые)Старый стандарт для мобильных
ASTCAdaptive Scalable Texture CompressionМобильные/VR (новые устройства)Гибкий, лучшее качество, настраиваемый
PVRTCPowerVRСтарые 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.

Важны не точные цифры в строке, а два вывода:

  1. Традиционные форматы (PNG/JPG/WebP) занимают почти одинаково видеопамяти — все распакованные исходные пиксели, 87 MB. На диске мелко, но видеопамять не экономится.
  2. KTX2 снижает видеопамять в 4–8 раз, при этом размер на диске не проигрывает.

Именно поэтому в VR, мобильном вебе почти обязательно используют KTX2 — сколько текстур по 87 MB поместится в 2 ГБ видеопамяти на телефоне? А по 11 MB — 7 штук.

Матрица поддержки платформ: какие GPU что поддерживают

Хотя Basis скрывает от нас детали, понимание базовой карты помогает в отладке. Вот поддержка родных форматов для современных устройств:

Платформа / устройствоBC1-7ETC2ASTCPVRTC
Десктоп 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, как настраивать параметры сжатия.

Поддержите нас