No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-05 17:07:48 +03:00
src * ver. 0.1.0 2026-09-05 17:07:48 +03:00
test_files * ver. 0.1.0 2026-09-05 17:07:48 +03:00
.gitattributes * ver. 0.1.0 2026-09-05 17:07:48 +03:00
.gitignore * ver. 0.1.0 2026-09-05 17:07:48 +03:00
Cargo.lock * ver. 0.1.0 2026-09-05 17:07:48 +03:00
Cargo.toml * ver. 0.1.0 2026-09-05 17:07:48 +03:00
README.md * ver. 0.1.0 2026-09-05 17:07:48 +03:00

packit — Sprite Atlas Packer

Упаковщик спрайтов в атласы с агрегацией в .images@<ppu>x_<fmt>.new.zip в формате референсного пайплайна ConvertAndRepack.

Формат атласов и atlas_data.jsonl совпадает с описанием из test_files/README.md.

Возможности

  • Упаковка webp-изображений (по layout.jsonl) в атласы
  • Форматы вывода: PNG, WebP (webp), ETC1, ETC2 (атлас — png / webp / pvr; для ETC1 дополнительно atlas_N_alpha.pvr при наличии прозрачности)
  • Рендер и кодирование изображений атласов: lossy webp (libwebp, VP8X+ALPH+VP8), ETC1/ETC2 в контейнер PVR v3 (заголовок и метадата побайтово как в референсе)
  • Детерминированный zip: фиксированные временные штампы (1980-01-01), без extra-полей — повторная упаковка тех же входов даёт бит-в-бит идентичный файл
  • Структура zip как в референсе: порядок записей (webp: .sprites.webp; etc1: .pvr.spritesatlas_N_alpha.pvr; etc2: .pvr.sprites), .webp/.png без сжатия (stored), .sprites/.pvr deflate, CRLF в .sprites
  • Результат: .images@1x_<fmt>.new.zip + .images@2x_<fmt>.new.zip (при --input-2x), .layouts@1x|2x.new.list, маркеры restype_uisprite.txt / unity_import_flag — как в референсе
  • Дедупликация по хэшу/размеру, tiled-спрайты → singleton wrap-атласы
  • Сравнение с референсными данными (test_files/atlas_data.jsonl)

Установка

cargo build --release

Использование

Упаковка папки с изображениями

cargo run --release -- pack --input test_files/input/rf_common_1x --input-2x test_files/input/rf_common_2x --format webp --out out --padding 2

Входная папка должна содержать спрайты (.webp) и layout.jsonl ({"name","x","y","width","height","hash"[,"tiled","scale"]} — источник истины).

Флаг Описание По умолчанию
--input Папка со спрайтами 1x + layout.jsonl
--input-2x Набор 2x, пакуется отдельно в .images@2x_…
--format Формат: png, webp, etc1, etc2
--quality Качество lossy webp (0100) 90
--out Папка для вывода out
--padding Отступ между спрайтами в пикселях 2
--max-size Максимальный размер атласа 4096

Сравнение с референсом

cargo run --release -- compare-jsonl --limit 1954
Флаг Описание По умолчанию
--jsonl Путь к jsonl test_files/atlas_data.jsonl
--filter Подстрока для фильтрации ключа
--limit Макс. строк для проверки 2000

Тест rf_common

Сравнение против референсных записей с "rf" и "common" в ключе:

cargo run --release -- test-rf-common --limit 20
Флаг Описание По умолчанию
--jsonl Путь к jsonl test_files/atlas_data.jsonl
--limit Макс. строк для проверки 2000

Формат вывода

out/
├── .images@1x_webp.new.zip      # atlas_N.webp + atlas_N.sprites
├── .images@2x_webp.new.zip      # при --input-2x
├── .layouts@1x.new.list         # name {Position=x y; Size=w h} <sha256>
├── .layouts@2x.new.list
├── restype_uisprite.txt
└── unity_import_flag

Состав повторяет референс; никаких дополнительных jsonl-файлов на выходе нет.

Для etc1/etc2 атласы — atlas_N.pvr (etc1: + atlas_N_alpha.pvr, только если в атласе есть прозрачность), для pngatlas_N.png.

PVR v3 (etc1/etc2)

52-байтовый little-endian заголовок (version 0x03525650, flags=0, глубина/поверхности/грани/mipmaps=1) + 15-байтовая метадата из референса, затем блоки ETC:

Формат pixelFormat Блок Размер данных
etc1 (цвет и альфа) 6 (ETC1_RGB8) 8 байт / 4×4 w·h/2
etc2 без прозрачности 22 (ETC2_RGB8) 8 байт / 4×4 w·h/2
etc2 с прозрачностью 23 (ETC2_RGBA8) 16 байт / 4×4 w·h

Альфа-текстура etc1 — альфа-канал, реплицированный в RGB и закодированный как ETC1. Размерности атласов дополняются до кратности 4 (размер ETC-блока).

Детерминизм

Повторная упаковка тех же входов даёт бит-в-бит идентичный zip: у всех записей фиксированное время 1980-01-01 00:00, extra-поля не пишутся, сжатие и порядок записей зависят только от содержимого.

test_files/atlas_data.jsonl

Референсный файл с метаданными упаковки (используется командами compare-jsonl / test-rf-common, на выходе pack не создаётся):

{"key":"/abs/path/rf_common/webp","atlases":[[1024,1024]],"sprites":[[768,513]],"positions":[[0,0,0,0]]}
Поле Тип Описание
key string Путь к папке (без .images_@2x.zip)
atlases [[w,h],...] Размеры атласов
sprites [[w,h],...] Размеры спрайтов
positions [[atlas_idx, sprite_idx, x, y],...] Позиции спрайтов в атласах

Алгоритм упаковки

  1. Дедупликация: одинаковые по размеру спрайты делят одну область атласа (как в референсе)
  2. MaxRects с портфолио из 4 правил размещения (Best Short/Long Side Fit, Best Area Fit, Bottom-Left)
  3. 13 порядков сортировки (по площади, сторонам, периметру, сторонам; в обе стороны)
  4. 3 стратегии score: минимальный абсолютный waste, минимальный относительный waste, максимум размещённых спрайтов
  5. Обрезка атласов по содержимому: точный bounding box (webp/etc2), округление до POT (etc1)
  6. Мульти-высотные кандидаты: для каждой ширины несколько высот (минимальная, POT, максимум) + «естественные» размеры спрайтов
  7. Принудительный первый атлас: перебор всех кандидатов в качестве первого (и двух первых для сложных наборов) с жадным хвостом
  8. Exact-поиск (backtracking) с бюджетом узлов: замыкает жадные тупики, которые MaxRects не видит (например, 8 decor-спрайтов в один 1024×4096)
  9. Subset DP для наборов ≤ 12 unique-спрайтов: оптимальное разбиение на атласы (3^n перебор разбиений поверх exact/greedy-упаковок подмножеств)
  10. Singleton-опция: по одному атласу на спрайт, если мульти-упаковка теряет площадь (как в референсе)
  11. Выбор минимальной суммарной площади атласов по всем комбинациям портфолио

Качество упаковки

Сравнение с test_files/atlas_data.jsonl (1954 наборов):

Формат Лучше Равно Хуже
etc1 226 257 0
etc2 474 9 0
webp 98 396 0
webp2x 98 396 0
  • 896 лучше / 1058 равно / 0 регрессий
  • Отношение площадей: 0.9385 (наши атласы = 93.9% от площади референса)
  • Все equal-случаи webp/webp2x достигают теоретического минимума (сумма площадей unique-спрайтов); etc1 equal — POT-минимумы (группировка не даёт выигрыша)
  • rf_common тест: 10 лучше / 6 равно / 0 хуже, ratio=0.6115

Производительность

Упаковка test_files/input (204 спрайта 1x + 204 2x, release): ~6 с для png/webp/etc2, ~3 с для etc1 — практически всё время уходит на декодирование входных webp и кодирование атласов; сама укладка — десятки миллисекунд.

Зависимости

  • image — загрузка webp, кодирование png
  • webp (libwebp) — lossy-кодирование атласов webp с альфа-каналом
  • ctt-etcpak — компрессия ETC1/ETC2
  • clap — CLI
  • zip — создание zip-архивов
  • serde / serde_json — JSONL
  • thiserror / anyhow — обработка ошибок

Сборка webp и ctt-etcpak требует C/C++-компилятора.

Структура проекта

src/
├── main.rs          # CLI, Error/Result, точка входа
├── packer.rs        # Алгоритм упаковки (MaxRects + exact + subset DP)
├── codec/mod.rs     # Форматы (Format, RgbaImage, load_dir)
├── codec/encode.rs  # Рендер атласов и кодирование (png/webp/etc1/etc2)
├── codec/pvr.rs     # Контейнер PVR v3 + метадата референса
├── layout.rs        # Парсинг входного layout.jsonl
├── compare.rs       # Сравнение с atlas_data.jsonl
├── bundle.rs        # Сборка .images@<ppu>x_<fmt>.new.zip + layouts-списков
└── sprites_file.rs  # Парсинг/запись .sprites метаданных