Kris

Первый урок по снижению веса модели: три техники сжатия вершин

3D-сжатиеСжатие вершинMeshOptDracoglTF

В предыдущей статье мы разобрали GLB-файл и увидели, что текстуры занимают 80% объёма, а вершины — всего 10–20%. Так что сжатие вершин может показаться неважным?

Как раз наоборот. Когда текстура уже сжата до KTX2, а вершины плотно упакованы, оставшиеся 20% — это вершины. И эти 20% можно урезать вдвое, а то и на 90%. Более того, сжатие вершин — одна из немногих оптимизаций, которая работает почти без затрат и даёт мгновенный эффект: добавил пару команд, сменил декодер — и файл похудел.

В этой статье разберём три вещи: как устроены данные вершин; особенности квантования, MeshOpt и Draco; и вывод, который поможет избежать ошибок — нет «лучшего» решения, есть «наиболее подходящее» для конкретной задачи.

Сколько весит одна вершина

Сначала посмотрим, что хранится в вершине. В glTF каждая вершина состоит из нескольких атрибутов:

АтрибутНазначениеТочность по умолчаниюБайт на вершину
position (позиция)Координаты вершины в пространстве3 × float3212
normal (нормаль)Определяет направление освещения3 × float3212
tangent (касательная)Расчёт карт нормалей4 × float3216
texcoord_0 (UV)Координаты текстуры2 × float328
color (цвет вершины)Окрашивание на уровне вершин4 × float3216

Вершина с полным набором атрибутов для PBR занимает 48–64 байта только на геометрию. Модель из 100 000 вершин — это 5–6 МБ только на вершины.

Обратите внимание: почти везде используется float32 (32-битное число с плавающей точкой). Это настройка по умолчанию, и именно здесь кроется возможность сжатия — большинству атрибутов 32-битная точность не нужна.

Первая техника: квантование (Quantization)

Квантование — основа всех методов сжатия вершин. Draco и MeshOpt тоже используют его внутри.

Квантование (quantization — отображение чисел с плавающей точкой на целые числа с меньшей разрядностью) по сути: число 3.14159265 можно запомнить как 3.14. Для набора координат в пространстве не нужно хранить каждый знак после запятой с 32-битной точностью — достаточно использовать целое число с меньшим диапазоном.

Исходно:   position.x = 1.234567   (float32, 4 байта)
После квантования: position.x = 1234   (int16, 2 байта) + scale/offset для восстановления

Сравнение до и после квантования:

АтрибутБайт во float32После квантования (16 бит)Экономия
position12650%
normal126 (или 4, с int8 + octahedral)50–67%
tangent164–850–75%
texcoord8450%

Для вершины из 48–64 байт после квантования получаем 16–24 байта — объём сокращается как минимум вдвое.

Когда использовать квантование

  • Вы хотите уменьшить размер, но не гонитесь за максимальным сжатием.
  • Вам нужна нулевая зависимость от декодера — квантованный glTF использует стандартное расширение KHR_mesh_quantization, которое поддерживается основными движками из коробки, без дополнительных библиотек.
  • Целевая платформа чувствительна к размеру пакета (например, VK Mini Apps или Telegram Mini Apps — каждый лишний килобайт декодера на счету).

Когда не стоит

  • Модель очень маленькая, а детали — ключевое преимущество (например, промышленные детали с точностью до миллиметра). Квантование особенно заметно на мелких моделях: текстура ещё нормально, но смещение позиции вершины на 0.1 мм становится видимым при крупном плане.

Реальная проблема точности квантования: в сцене с демонстрацией ювелирных изделий после квантования кольца до 16 бит появились зубцы на металлических краях при крупном плане. Причина не в недостатке вершин, а в том, что мировая система координат слишком мала, и 16-битное целое не может обеспечить достаточную детализацию. Решение — уменьшить диапазон квантования (сжать bounding box position) или использовать большую разрядность для маленьких моделей.

Вторая техника: MeshOpt

MeshOpt — это расширение glTF EXT_meshopt_compression, которое позиционируется как «хорошая степень сжатия и молниеносное декодирование».

Сначала атрибуты квантуются (как выше), а затем применяется LISS (lossless entropy coding — сжатие без потерь с энтропийным кодированием), чтобы дополнительно сжать уже квантованные целые числа без потерь. Иными словами: квантование с потерями + энтропийное кодирование без потерь = меньший размер и то же качество, что и при квантовании.

  • Степень сжатия: на 30–50% меньше, чем просто квантование.
  • Скорость декодирования: очень высокая, реализация на чистом C/JS, десятки миллионов вершин в секунду на одном потоке.
  • Размер декодера: очень маленький (около 20–30 КБ в gzip).
  • Совместимость: Three.js, Babylon.js поддерживают из коробки — это де-факто стандарт для веба.

Когда использовать MeshOpt

  • Нужна более высокая степень сжатия, но не хочется мириться с медленным декодированием, как у Draco.
  • Основная платформа — веб, мобильные устройства, WebXR — скорость декодирования напрямую влияет на время загрузки первого экрана.
  • Модели часто распаковываются (например, динамически загружаемые уровни).

Когда не стоит

  • Ваша целевая платформа не поддерживает EXT_meshopt_compression (очень старые движки).
  • Вас устраивает разница в 30% и вы хотите максимально простое решение — тогда чистое квантование проще, меньше зависимостей.

Третья техника: Draco

Draco — решение от Google, нацеленное на «максимальную степень сжатия».

Его главное отличие от двух предыдущих: Draco изменяет топологию соединения вершин. Квантование меняет только числовое представление каждой вершины, MeshOpt добавляет к нему кодирование без потерь, а Draco перестраивает треугольную сетку, чтобы компактнее описать, «какие вершины образуют треугольники».

  • Степень сжатия: самая высокая среди трёх, для плотных моделей часто достигает более 90% сокращения.
  • Скорость декодирования: самая низкая из трёх, но всё равно быстрая (относительно).
  • Размер декодера: большой (около 100–200 КБ, обычно требуется загружать wasm отдельно).
  • Качество: настраивается, но при экстремальном сжатии возможны видимые искажения.

Когда использовать Draco

  • Модели очень большие, с огромным количеством вершин (миллионы вершин в отсканированных моделях, ландшафтах).
  • Однократная загрузка, модель используется долго после распаковки (небольшая задержка декодирования допустима).
  • Размер пакета не критичен, а скорость загрузки по сети — узкое место.

Когда не стоит

  • Мобильные устройства + необходимость быстрого первого экрана — декодер и сама модель загружаются одновременно, что может замедлить процесс.
  • Среда с жёсткими ограничениями на размер пакета (например, VK Mini Apps, Telegram Mini Apps).
  • Модели с анимацией скелета (skinning) или morph targets — Draco поддерживает их слабо, неправильные настройки могут привести к ошибкам.

Сравнение трёх методов: таблица выбора

Коэффициенты сжатия взяты из бенчмарков сообщества (обсуждения на Habr, Reddit r/threejs), для разных моделей они могут отличаться, но относительные соотношения стабильны:

МетодСтепень сжатия (относительно float32)Скорость декодированияРазмер декодераС потерями?Расширение glTF
Квант.~50%Нативно, без декодера0Да (точность)KHR_mesh_quantization
MeshOpt~25–35%Очень высокая~25 КБДа (точность)EXT_meshopt_compression
Draco~10–20%Высокая (самая низкая)~100–200 КБДа (точность+топология)KHR_draco_mesh_compression

Совместимость с платформами:

ПлатформаКвантованиеMeshOptDraco
Веб (десктоп)✅ Нативно✅ Нативно✅ нужен декодер
Веб (мобильный)✅ Нативно✅ Нативно⚠️ декодер тяжёлый
WebXR/VR✅ Нативно✅ Рекомендуется⚠️ осторожно
VK Mini Apps / Telegram Mini Apps✅ Рекомендуется✅ Рекомендуется❌ по возможности избегать

Краткий итог: хотите просто и без зависимостей → чистое квантование; хотите баланс → MeshOpt; нужно максимальное сжатие и готовы ждать → Draco.

Практика: используем gltfpack для квантования и MeshOpt

gltfpack — официальный инструмент glTF, одной командой делает и квантование, и MeshOpt.

Установка (бинарники можно скачать с релиза gltfpack):

# Квантуем model.glb до 16 бит и добавляем сжатие MeshOpt
gltfpack -i model.glb -o model-packed.glb -cc

# -cc = compress (поверх квантования по умолчанию добавляет EXT_meshopt_compression)

Часто используемые параметры:

# Только квантование, без MeshOpt (самый лёгкий, без зависимостей от декодера)
# gltfpack по умолчанию делает 16-битное квантование вершин (KHR_mesh_quantization), дополнительных параметров не нужно
gltfpack -i model.glb -o model-quant.glb

# Квантование + MeshOpt
gltfpack -i model.glb -o model-meshopt.glb -cc

# Если вершин очень много, можно упростить модель (уменьшить количество вершин, изменит геометрию)
gltfpack -i model.glb -o model-simplify.glb -cc -si 0.5
# -si 0.5 — упростить примерно до 50% вершин

О -cc: это переключатель «compress», который добавляет EXT_meshopt_compression. Без -cc gltfpack по умолчанию делает только квантование — то есть команда gltfpack -i in.glb -o out.glb уже даёт «чистое квантование, без зависимостей от декодера». (Не путать с -v — verbose, подробный лог.)

Типичный результат (модель PBR размером 5 МБ, 120 000 вершин, для справки):

ОбработкаРазмер файлаОписание
Исходный (float32)5.0 МББазовый
Чистое квантование2.6 МБУменьшение вдвое, визуально без разницы
MeshOpt (-cc)1.7 МБЕщё на 35% меньше, загрузка чуть быстрее

Обратите внимание: -si — это упрощение с изменением геометрии модели, с потерями, и это не то же самое, что сжатие. Сжатие старается сохранить визуальное качество, а упрощение намеренно уменьшает детализацию. Их можно комбинировать, но нужно учитывать, допустимо ли это для сцены.

Частые ошибки

  • После квантования нормали «перевернулись»: скорее всего, использована слишком низкая точность. Для normal используйте как минимум 16 бит или 8-битное октаэдрическое кодирование.
  • После декодирования Draco пропали материалы: Draco сжимает только сетку, материалы и текстуры нужно обрабатывать отдельно. При загрузке требуется одновременно подключить и декодер Draco, и расширение KHR.
  • В VK Mini Apps / Telegram Mini Apps Draco не загружается: декодер в виде wasm может быть ограничен в некоторых средах выполнения. Замена на MeshOpt обычно решает проблему.
  • Модель «уплыла» после квантования: когда модель находится далеко от начала координат, 16-битной точности может не хватить для больших координат и мелких деталей. Решение: переместить модель ближе к началу координат перед квантованием или увеличить разрядность.

Что дальше

Вершины сжаты, но не спешите радоваться — как мы говорили, текстуры занимают 80% объёма модели. В следующей статье сменим поле битвы и посмотрим, почему традиционные PNG/JPG в глазах GPU — «обжоры», и как с этим справляется нативный формат текстур GPU.

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