- C++ 57%
- Rust 43%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .cargo | ||
| .forgejo/workflows | ||
| .zed | ||
| src | ||
| test_files | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
packit — Sprite Atlas Packer
Упаковщик спрайтов в атласы с агрегацией в .images@<ppu>x_<fmt>.new.zip в формате референсного пайплайна ConvertAndRepack.
Формат атласов и atlas_data.jsonl совпадает с описанием референсных данных test_files/rf_common/AGENTS.md.
Возможности
- Упаковка webp-изображений (по
layout.jsonl) в атласы - Несколько форматов за один запуск (
--format webp etc1 etc2), форматы пакуются параллельно на потоках - Per-format выбор набора: суффиксы
webp@1x/etc1@2x; без суффикса — 1x и 2x - Баланс «площадь ↔ число атласов» через
--area-tolerance(по умолчанию 5%): меньше файлов при почти той же площади - Шапка с входными данными (наборы, число спрайтов, форматы), простая ASCII-анимация (спиннер) во время упаковки и итоговый отчёт с общим временем выполнения; сводная таблица (атласы / площадь / fill / размер зипа / экономия сжатия / время)
- Папка
--outсоздаётся по полному пути и очищается перед упаковкой (с защитой от--out= входная папка) - Форматы вывода: PNG, WebP (
webp), ETC1, ETC2 (атлас —png/webp/pvr; для ETC1 дополнительноatlas_N_alpha.pvrпри наличии прозрачности); в режиме--restype sprite— те же форматы по одному файлу на спрайт (name.webp/name.pvr+name_alpha.pvr) - Рендер и кодирование изображений атласов: lossy webp (libwebp, VP8X+ALPH+VP8; настройки по умолчанию как у референсного
cwebp -mt -m 6 -q 75 -pass 10, кроме двух отклонений, лечащих межспрайтовое протекание цвета на merge_raider (зелёная кнопка окрашивала teal-ячейку ниже, видно в игре при 2x):filter_strength = 0(cwebp -f 0) — деблокинг VP8 перетаскивает хрому AA-краёв спрайта в непрозрачные строки соседа через 16px-границу макроблока;exact = 1— с дефолтным exact=0 libwebp произвольно перекрашивает RGB прозрачных пикселей («призрак» кнопки расползается по пустым зонам, экономя биты). Вместе: тинт кромки 22.0 → 0.84 (blue-drop 36 → 2) ценой ~2% размера; filter 0 дополнительно даёт +0.4 dB PSNR на непрозрачных пикселях), ETC1/ETC2 в контейнер PVR v3 (заголовок и метадата побайтово как в референсе) - ETC-энкодер на выбор (
--etc-encoder):playrix— брутфорс-ядро Playrix Etc*Compress (MIT) с перцептивной метрикой и SSIM-отбором; на rf_common@1x даёт ~+1.7 дБ (ETC1) и ~+1.9 дБ (ETC2) к wPSNR референс-пайплайна, ценой ~2× времени пайплайна;etcpak— быстрые эвристики etcpak (как в референсе по скорости). По умолчаниюplayrixна x86_64 unix (ядро SSE4.1), на прочих целях —etcpak - Детерминированный zip: фиксированные временные штампы (1980-08-16), без extra-полей — повторная упаковка тех же входов даёт бит-в-бит идентичный файл
- Структура zip как в референсе: порядок записей (
webp:.sprites→.webp;etc1:.pvr→.sprites→atlas_N_alpha.pvr;etc2:.pvr→.sprites),.webp/.pngбез сжатия (stored),.sprites/.pvrdeflate уровня 12 (zip-крейт: уровни >9 — это zopfli, 12 = 3 итерации), CRLF в.sprites. Zopfli×3 экономнее референсного7z -tzip -mx=9: на rf_common@1x etc1- и etc2-зипы на ~2% меньше референсных при том же контенте, ценой ~1.5 с/МБ payload (однопоточно, после параллельного кодирования) - Результат:
.images@1x_<fmt>.new.zip+.images@2x_<fmt>.new.zip(при--input-2x),.layouts@1x|2x.new.list, маркерыrestype_uisprite.txt/unity_import_flag— как в референсе - Дедупликация по
hashизlayout.jsonl(идентичность движка; фолбэк — хэш пикселей),tiled-спрайты → singleton wrap-атласы - Спрайты с
#в имени: в атласных режимах пакуются как обычные (так делает референс — все атласы find_objects содержат#-слои); в режиме--restype spriteне конвертируются и не попадают в zip (референс удаляет loose#*.png) - Сравнение с референсными данными (
test_files/atlas_data.jsonl)
Превью (ImageMagick)
Генерация превью-миниатюр сцен из PSD — как -previews референсного конвейера: слои сливаются, берётся кроп 1536x1536+256+0, результат сжимается до высоты 144px (ppu 1) или 288px (ppu 2), PNG q95 без метаданных, имя = имя PSD.
packit preview --input "res/locations/preview" --out previews --height 144
| Флаг | Описание | По умолчанию |
|---|---|---|
--input |
PSD-файлы или папки с PSD (без рекурсии) | — (обязателен) |
--out |
Общая папка превью | previews |
--height |
Высота превью в пикселях | 144 |
--crop |
Кроп W:H:X:Y после слияния слоёв |
1536:1536:256:0 |
--magick |
Бинарник ImageMagick | magick |
Требует ImageMagick в PATH (или путь через --magick); пиксельный результат зависит от версии ImageMagick.
Фоны (ImageMagick)
Масштабированные фоны сцен из PSD — порт референсной ветки -custom-scale:W:H:type: слои сливаются, масштабируются по режиму (fill / shrinklarger / shrinkcustom / none, с центральным кропом до целевого размера), результат — background.png + background {Rect=0 0 w h} мета + zip с webp, по подпапке на PSD.
packit background --input "res/backgrounds" --out out/backgrounds --width 1536 --height 1024 --scale-type fill
| Флаг | Описание | По умолчанию |
|---|---|---|
--input |
PSD-файлы или папки с PSD (без рекурсии) | — (обязателен) |
--out |
Корневая папка (подпапка на PSD) | backgrounds |
--width / --height |
Целевой логический размер | 1536 / 1024 |
--scale-type |
Режим масштабирования: fill, shrinklarger, shrinkcustom, none |
fill |
--ppu |
Множитель целевого размера (ppu ≠ 1) | 1 |
--quality |
webp-качество фона | 75 |
--magick |
Бинарник ImageMagick | magick |
Установка
cargo build --release
Использование
Упаковка папки с изображениями
# один формат
cargo run --release -- pack --input test_files/input/rf_common_1x --format webp --out out --padding 2
# несколько форматов за один запуск (параллельно, по потоку на формат)
cargo run --release -- pack --input test_files/input/rf_common_1x --input-2x test_files/input/rf_common_2x --format webp etc1 etc2 --out out
# per-format выбор набора: webp — 1x и 2x, etc1 — только 1x, etc2 — только 2x
cargo run --release -- pack --input test_files/input/rf_common_1x --input-2x test_files/input/rf_common_2x --format webp etc1@1x etc2@2x --out out
Входная папка должна содержать спрайты (.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; опциональный суффикс набора: webp@1x — только 1x, etc1@2x — только 2x, без суффикса — 1x и 2x. Несколько значений или повторов флага; повторы объединяются (webp webp@1x → оба набора) |
— (обязателен, ≥1) |
--quality |
Качество lossy webp, cwebp -q (0–100) |
75 |
--webp-method |
Усилие сжатия webp, cwebp -m (0–6) |
6 |
--webp-pass |
Мультипроходный анализ webp, cwebp -pass (1–10) |
10 |
--sharp-yuv |
Более резкое (но медленное) RGB→YUV преобразование для lossy webp, cwebp -sharp_yuv |
выкл. (как в референсе) |
--etc-encoder |
Кодировщик ETC-блоков: playrix (качество, медленнее) или etcpak (быстро) |
playrix (x86_64 unix), иначе etcpak |
--out |
Папка для вывода (создаётся по полному пути; перед упаковкой очищается) | out |
--append |
Не очищать --out (многопроходная сборка в одну папку, например locations/decors: webp-одиночки + etc-атласы) |
выкл. |
--no-layouts |
Не писать .layouts@…new.list (для потребителей, берущих layout-данные из другого источника) |
выкл. |
--padding |
Отступ между спрайтами в пикселях | 2 |
--max-size |
Максимальный размер атласа (в режиме sprite не применяется) |
4096 |
--restype |
Тип импорта: uisprite (атласы, по умолчанию), scenesprite (то же с другим маркером), sprite — без атласов: каждый спрайт = отдельный файл в том же zip + per-sprite .sprites; маркер restype_sprite.txt |
uisprite |
--area-tolerance |
Допуск по площади атласов (%) ради меньшего числа файлов (0 — строгий минимум площади, даёт много мелких атласов) |
5 |
--verbose |
Печатать список атласов (размеры и число спрайтов) | выкл. |
Перед упаковкой печатается шапка с входными данными: наборы (@1x/@2x) с числом спрайтов (быстрый pre-scan без декодирования), выбранные форматы и папка вывода:
packit 2026.10.16 - 2 set(s), 3 format(s)
@1x test_files/input/rf_common_1x 203 sprites
@2x test_files/input/rf_common_2x 203 sprites
formats: webp, etc1 (@1x), etc2 (@2x)
out: out/
Во время работы в терминале — простая ASCII-анимация-спиннер (| / - \ packing sprites...) без процентов, ETA и описания текущего действия; только ASCII-символы, поэтому одинаково работает в консолях Windows и Linux. Живой анимации нужен настоящий терминал на stderr; где его нет (панель запуска/отладки IDE, пайп, CI, TERM=dumb), вместо спиннера печатаются построчные события — старт загрузки набора, каждый закодированный атлас ([webp] atlas 2/5: ...), завершение формата.
После каждого набора — сводная таблица: число атласов, суммарная площадь, заполненность (fill), размер зипа против несжатого объёма (saved), время формата и wall time:
== test_files/input/rf_common_1x @1x : 203 sprites (201 normal, 2 tiled)
format atlases area fill zip payload saved time
webp 5 2.12Mpx 91.3% 405.2 KiB 410.9 KiB 1.4% 4.1s
etc1 4 2.16Mpx 89.5% 473.9 KiB 2.07 MiB 77.7% 2.3s
set: 2 zips, 879.1 KiB, wall 4.1s
В самом конце — итог всего запуска: суммарные атласы/зипы/размер и общее время выполнения с разбивкой на загрузку и упаковку+кодирование:
done: 2 set(s), 406 sprites -> 26 atlases in 6 zip(s), 5.77 MiB (payload 22.48 MiB, saved 74.3%)
written to out in 8.5s total (load 0.2s, pack+encode 8.3s)
Метрики в отчётах
Колонки таблицы набора:
| Колонка | Значение |
|---|---|
format |
формат атласов (png / webp / etc1 / etc2) |
atlases |
число атласов в бандле, включая {Wrap}-синглтоны для tiled-спрайтов |
area |
суммарная площадь атласов (Σ W×H); Mpx — мегапиксели |
fill |
заполненность: площадь уникального контента спрайтов к area в % (100% = без пустот; реально чуть ниже из-за --padding и неплотной укладки) |
zip |
итоговый размер .images@<ppu>_<fmt>.new.zip на диске |
payload |
суммарный размер записей внутри зипа без сжатия (атласы + .sprites + atlas_N_alpha.pvr) |
saved |
экономия сжатия zip-контейнера: 100% × (1 − zip / payload). У webp/png мала (атласы хранятся stored — уже сжаты кодеком, дефлейтится только текст .sprites); у etc1/etc2 велика (70–85%, pvr-блоки хорошо сжимаются) |
time |
время потока формата: укладка + кодирование + запись. Форматы пакуются параллельно, поэтому сумма time по форматам больше wall |
Строка set: — итог набора: число зипов, их суммарный размер и wall — реальное (по часам) время этапа упаковки набора, от старта потоков форматов до завершения последнего. Равна критическому пути (самый медленный формат + накладные расходы на потоки); загрузка/декодирование спрайтов в wall не входит.
Итог запуска (последние две строки):
| Параметр | Значение |
|---|---|
done: … sprites -> … atlases in … zip(s) |
агрегаты по всем наборам: спрайты (normal + tiled), атласы, зипы |
… MiB (payload …, saved …%) |
суммарный размер зипов против несжатого объёма и общая экономия сжатия |
total |
общее время выполнения: от старта до записи всех артефактов |
load |
суммарное время загрузки и декодирования входных наборов |
pack+encode |
суммарное wall всех наборов (укладка + кодирование + запись) |
С --verbose после таблицы печатается список артефактов: имя зипа и по каждому атласу строка atlas_N: WxH (M sprites).
Сравнение с референсом
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 |
Обе команды сравнения печатают одинаковую сводку:
sets: 6 better=6 equal=0 worse=0
total area: ref=23897184 ours=14200314 ratio=0.5942
| Параметр | Значение |
|---|---|
sets |
число проверенных наборов референса |
better / equal / worse |
у скольких наборов наша суммарная площадь атласов меньше / равна / больше референсной |
ref / ours |
суммарная площадь атласов по всем наборам: референс и наша |
ratio |
ours / ref — отношение площадей; меньше 1 — в среднем компактнее референса |
Формат вывода
out/
├── .images@1x_webp.new.zip # atlas_N.webp + atlas_N.sprites
├── .images@1x_etc1.new.zip # по одному зипу на каждый --format
├── .images@1x_etc2.new.zip
├── .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, только если в атласе есть прозрачность), для png — atlas_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-08-16 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],...] |
Позиции спрайтов в атласах |
Алгоритм упаковки
- Дедупликация: спрайты с одинаковым
hashизlayout.jsonl(идентичность контента движка; у imaginum это blake3 пиксельных байт) делят одну область атласа; без хэша в layout — фолбэк на FNV-1a декодированных пикселей. Совпадение только размеров содержимое не объединяет - MaxRects с портфолио из 5 правил размещения (Best Short/Long Side Fit, Best Area Fit, Bottom-Left, Contact Point)
- 13 порядков сортировки (по площади, сторонам, периметру, сторонам; в обе стороны)
- 3 стратегии score: минимальный абсолютный waste, минимальный относительный waste, максимум размещённых спрайтов
- Обрезка атласов по содержимому: точный bounding box (webp/etc2), округление до POT (etc1)
- Мульти-высотные кандидаты: для каждой ширины несколько высот (минимальная, POT, максимум) + «естественные» размеры спрайтов
- Принудительный первый атлас: перебор всех кандидатов в качестве первого (и двух первых для сложных наборов) с жадным хвостом
- Exact-поиск (backtracking) с бюджетом узлов: замыкает жадные тупики, которые MaxRects не видит (например, 8 decor-спрайтов в один 1024×4096)
- Subset DP для наборов ≤ 12 unique-спрайтов: оптимальное разбиение на атласы (3^n перебор разбиений поверх exact/greedy-упаковок подмножеств)
- Singleton-опция: по одному атласу на спрайт (как в референсе) — только если мульти-упаковка выходит за допуск по площади
- Count-aware выбор: среди всех результатов портфолио, лежащих в допуске
--area-tolerance(по умолчанию 5%) от минимальной площади, выбирается вариант с наименьшим числом атласов; при--area-tolerance 0— строгий минимум площади - Merge-pass: жадное слияние мелких атласов (через точную упаковку объединения), пока суммарная площадь не выходит за допуск от исходной
Качество упаковки
Сравнение с 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
Итоговые зипы rf_common (рабочий режим, --area-tolerance 5) против референсных бандлов:
| Бандл | packit | Референс | Разница |
|---|---|---|---|
| webp 1x | 415 КБ (5 атласов) | 940 КБ (3) | 2.3× меньше |
| webp 2x | 1.08 МБ (4 атласа) | 2.49 МБ (3) | 2.3× меньше |
| etc1 1x | 485 КБ (4 атласа) | 907 КБ (6) | 1.9× меньше |
| etc2 1x | 586 КБ (5 атласов) | 1.04 МБ (7) | 1.8× меньше |
(у референса бандлов 2x для etc1/etc2 нет; два «лишних» атласа в наших webp-бандлах — обязательные {Wrap}-синглтоны для tiled-спрайтов, как и у референса)
Производительность
Упаковка rf_common (203 спрайта 1x + 203 2x, форматы webp/etc1/etc2, release): ~4 с на набор (форматы кодируются параллельно, по потоку на формат), ~8 с на полный запуск 1x+2x. Сама укладка — десятки миллисекунд, практически всё время уходит на декодирование входных webp и кодирование атласов; входы декодируются один раз на набор и переиспользуются всеми форматами.
Осознанные отказы (функции референсного конвейера, не поддерживаемые)
--use-rectmask(бинпакер, 1 PSD в дереве) — не используется- PSD-оверрайды
name@ppu.psd— пер-PPU подмена исходника -skip-textures(только списки без текстур), форматы jng/dxt/pvrtc/rgba,-locлокализация (переговорные слои,.lang-зипы)-remote,-generate-previewsкак флаги основного пака — вместо них отдельные подкомандыpreview/background
Зависимости
image— загрузка webp, кодирование pnglibwebp-sys(libwebp) — lossy-кодирование атласов webp с альфа-каналом (advanced API:-q/-m/-pass/-mtкак в cwebp)ctt-etcpak— компрессия ETC1/ETC2 (быстрый путь)src/codec/playrix/— вендоренные ядра Playrix EtcCompress/Etc2Compress (MIT, © Playrix LLC), собираютсяbuild.rs(cc, SSE4.1, только x86_64 unix); вызовы идут на выделенном потоке с 64 МБ стеком, воркеры ядер переведены на последовательное выполнение,Stride— thread_local (packit кодирует атласы параллельно)clap— CLIindicatif— спиннер-анимация в терминалеzip— создание zip-архивовserde/serde_json— JSONLthiserror/anyhow— обработка ошибок
Сборка webp и ctt-etcpak требует C/C++-компилятора.
Структура проекта
src/
├── main.rs # Точка входа: Error/Result, диспетчер подкоманд
├── cli.rs # Определения clap (Cli/Cmd/PackArgs) + разбор `--format` спеков
├── pack.rs # Оркестрация `pack`: наборы → загрузка → потоки форматов → отчёты
├── packer.rs # Алгоритм упаковки (MaxRects + exact + subset DP)
├── progress.rs # Спиннер-анимация упаковки (+ построчный fallback без терминала)
├── report.rs # Консольные отчёты: шапка входов, таблицы наборов, итог прогона
├── 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-списков