Any3DAny3D
·Any3D Team

Текстуры — прожорливое чудовище, пожирающее ваш VRAM

3d-compressiontexture-compressionwebglwebgpu

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

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

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

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

Форматы текстур, родные для GPU: сжатые в VRAM по своей природе

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

Именно это делают форматы текстур, родные для GPU. Основные семейства:

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

Суть не в точном числе той или иной строки, а в двух вещах:

  1. Традиционные форматы (PNG/JPG/WebP) имеют практически идентичное использование VRAM — все распакованные сырые пиксели, 87 МБ. Каким бы маленьким ни был файл на диске, VRAM не экономится.
  2. KTX2 снижает VRAM до 1/4 — 1/8, и размер на диске тоже конкурентоспособен.

Именно поэтому VR и мобильный веб почти всегда используют KTX2 — сколько текстур по 87 МБ поместится в 2 ГБ VRAM телефона? При 11 МБ помещается семь.

Матрица поддержки платформ: какие GPU распознают какие форматы

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

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

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