Первый урок по снижению веса модели: три техники сжатия вершин
В предыдущей статье мы разобрали GLB-файл и увидели, что текстуры занимают 80% объёма, а вершины — всего 10–20%. Так что сжатие вершин может показаться неважным?
Как раз наоборот. Когда текстура уже сжата до KTX2, а вершины плотно упакованы, оставшиеся 20% — это вершины. И эти 20% можно урезать вдвое, а то и на 90%. Более того, сжатие вершин — одна из немногих оптимизаций, которая работает почти без затрат и даёт мгновенный эффект: добавил пару команд, сменил декодер — и файл похудел.
В этой статье разберём три вещи: как устроены данные вершин; особенности квантования, MeshOpt и Draco; и вывод, который поможет избежать ошибок — нет «лучшего» решения, есть «наиболее подходящее» для конкретной задачи.
Сколько весит одна вершина
Сначала посмотрим, что хранится в вершине. В glTF каждая вершина состоит из нескольких атрибутов:
| Атрибут | Назначение | Точность по умолчанию | Байт на вершину |
|---|---|---|---|
| position (позиция) | Координаты вершины в пространстве | 3 × float32 | 12 |
| normal (нормаль) | Определяет направление освещения | 3 × float32 | 12 |
| tangent (касательная) | Расчёт карт нормалей | 4 × float32 | 16 |
| texcoord_0 (UV) | Координаты текстуры | 2 × float32 | 8 |
| color (цвет вершины) | Окрашивание на уровне вершин | 4 × float32 | 16 |
Вершина с полным набором атрибутов для 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 бит) | Экономия |
|---|---|---|---|
| position | 12 | 6 | 50% |
| normal | 12 | 6 (или 4, с int8 + octahedral) | 50–67% |
| tangent | 16 | 4–8 | 50–75% |
| texcoord | 8 | 4 | 50% |
Для вершины из 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 |
Совместимость с платформами:
| Платформа | Квантование | MeshOpt | Draco |
|---|---|---|---|
| Веб (десктоп) | ✅ Нативно | ✅ Нативно | ✅ нужен декодер |
| Веб (мобильный) | ✅ Нативно | ✅ Нативно | ⚠️ декодер тяжёлый |
| 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. Без-ccgltfpack по умолчанию делает только квантование — то есть команда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.