Any3DAny3D
·Any3D Team

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

3d-compressionvertex-compressionmeshoptdracogltf

В предыдущей статье мы заглянули внутрь 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 (vertex color)Посимвольная закраска4 × float3216

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

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

Оружие первое: квантизация

Квантизация — это основной принцип любого сжатия вершин; Draco и MeshOpt также используют её внутри.

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

Оригинал:   position.x = 1.234567   (float32, 4 байта)
Квантизация: position.x = 1234       (int16,   2 байта)  + масштаб/смещение для восстановления

До и после квантизации:

Атрибутfloat32 байтКвантизация (16 бит)Экономия
position12650%
normal126 (или 4, при использовании int8 + октаэдральная)50-67%
tangent164-850-75%
texcoord8450%

Для вершины размером 48-64 байта квантизация сжимает её до 16-24 байт, уменьшая размер более чем вдвое.

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

  • Вы просто хотите уменьшить размер и не преследуете максимальный коэффициент сжатия
  • Вам нужна нулевая зависимость от декодера — квантизированный glTF использует стандартное расширение KHR_mesh_quantization, поддерживаемое основными движками без необходимости подключать дополнительную библиотеку декодера
  • Ваша целевая платформа чувствительна к размеру пакета (например, WeChat Mini Programs, где подключение декодера Draco стоит десятки КБ)

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

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

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

Оружие второе: MeshOpt

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

Что он делает: сначала квантизирует атрибуты (как описано выше), затем применяет технику под названием кодирование по энтропии (без потерь) для беспотерьного повторного сжатия квантизованных целых. Иными словами: сжатие с потерями (квантизация) + беспотерьное энтропийное кодирование = меньший размер, качество идентичное простой квантизации.

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

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

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

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

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

Оружие третье: Draco

Draco — решение для сжатия от Google, позиционируемое как «максимальный коэффициент сжатия».

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

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

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

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

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

  • Мобильные устройства + необходимость быстрого первого рендера — придётся загружать и декодер, и модель, что затягивает процесс
  • Строгие ограничения по размеру пакета, такие как Mini Programs
  • Модели с анимацией скинга, морф-таргетами — поддержка Draco для этого слабая, а неправильная конфигурация вызывает проблемы

Все три рядом: таблица сравнения

Коэффициенты сжатия ниже взяты из.community-бенчмарков (тесты DeepKolos + обсуждения Reddit r/threejs). Разные модели отличаются, но относительные соотношения стабильны:

ВариантКоэффициент сжатия (vs 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✅ Нативно✅ Рекомендуется⚠️ Использовать с осторожностью
WeChat Mini Program✅ Рекомендуется✅ Рекомендуется❌ По возможности избегать

Однострочное резюме: хотите просто и без зависимостей — простая квантизация; хотите баланс — MeshOpt; хотите максимальный коэффициент и готовы подождать — Draco.

Практика: квантизация и MeshOpt с gltfpack

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

Сначала установка (бинарники доступны на странице релизов gltfpack):

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

# -cc = сжатие (добавляет 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: это ключ «сжатие», который дополнительно применяет EXT_meshopt_compression. Без -cc gltfpack всё равно квантизирует по умолчанию — то есть gltfpack -i in.glb -o out.glb сам по себе уже является «простой квантизацией, нулевая зависимость от декодера». (-v — флаг подробного логирования, не путайте.)

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

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

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

Типичные ошибки

  • Направление нормалей изменилось после квантизации: обычно слишком низкая точность. Используйте минимум 16 бит для нормалей или 8-битную октаэдральную кодировку.
  • Материалы потерялись после декодирования Draco: Draco сжимает только сетки; материалы и текстуры необходимо обрабатывать отдельно. При загрузке необходимо настроить как декодер Draco, так и расширения KHR.
  • Draco не загружается в Mini Program: декодер wasm ограничен в некоторых средах выполнения; переключение на MeshOpt обычно решает проблему.
  • Модель «плывёт» после квантизации: когда модель далеко от начала координат, 16-битная точность не может одновременно выразить и большие координаты, и мелкие детали. Решение — переместить модель ближе к началу координат перед квантизацией или увеличить разрядность.

Что дальше

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

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