Похудение модели, урок первый: Три оружия сжатия вершин
В предыдущей статье мы заглянули внутрь 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 (vertex color) | Посимвольная закраска | 4 × float32 | 16 |
Вершина с полным набором 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 бит) | Экономия |
|---|---|---|---|
| position | 12 | 6 | 50% |
| normal | 12 | 6 (или 4, при использовании int8 + октаэдральная) | 50-67% |
| tangent | 16 | 4-8 | 50-75% |
| texcoord | 8 | 4 | 50% |
Для вершины размером 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 |
Декодеры и совместимость с платформами:
| Платформа | Простая квантизация | MeshOpt | Draco |
|---|---|---|---|
| Десктопный веб | ✅ Нативно | ✅ Нативно | ✅ Требуется настройка декодера |
| Мобильный веб | ✅ Нативно | ✅ Нативно | ⚠️ Декодер тяжёлый |
| 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. Без-ccgltfpack всё равно квантизирует по умолчанию — то есть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, решают эту проблему.