Разбор атласа Тени: алгоритм оригинала, замеры палитры, план
XOR у оригинала идёт по 24-битному RGB, а blitters_2_or — обычный блит с colour key 0. От фона зависит только кайма в один пиксель по левым кромкам силуэта; на чёрном фоне запечка точна. Замеры: 253 кадра (Кид 219 + страж 34, Тень в боевых кадрах рисуется спрайтами СТРАЖА), 59 разных цветов. 32 цвета оставляют перцептивно значимыми 232 пикселя из 96 746. Палитра: занято 112 слотов, свободно 144; берём 0xA0..0xBF.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# Атлас Тени — разбор и план
|
||||
|
||||
Дата: 2026-08-20. Статус: **разбор сделан, код не писали.**
|
||||
|
||||
Задача: сейчас Тень рисуется спрайтами Кида (вне боя) и спрайтами стража
|
||||
(в бою) как есть, поэтому от Кида она не отличается. В оригинале её вид
|
||||
даёт наложение спрайта на себя со сдвигом; наш пакетный блит такого не
|
||||
умеет, поэтому запекаем результат в отдельный атлас заранее.
|
||||
|
||||
## 1. Что делает оригинал (проверено по SDLPoP, не по памяти)
|
||||
|
||||
`draw_objtable_item`, `seg008.c:1600`:
|
||||
|
||||
```c
|
||||
case 1: // shadow
|
||||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl, obj_y, blitters_2_or, 1);
|
||||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl + 1, obj_y, blitters_3_xor, 1);
|
||||
```
|
||||
|
||||
Тот же самый спрайт кладётся ДВАЖДЫ: первый проход в x, второй в x+1.
|
||||
Два уточнения, которые меняют алгоритм запекания:
|
||||
|
||||
1. **`blitters_2_or` — это НЕ побитовое ИЛИ.** В SDLPoP он реализован
|
||||
обычным блитом с colour key = индекс 0 (`method_6_blit_img_to_scr`,
|
||||
`seg009.c:3306`: `SDL_SetColorKey(image, SDL_TRUE, 0)`). То есть
|
||||
первый проход — наш обычный прозрачный блит, один в один.
|
||||
2. **`blitters_3_xor` работает по 24-битному RGB, а не по индексам
|
||||
палитры** (`blit_xor`, `seg009.c:3190`: конвертация в 24 бита, затем
|
||||
`*p_dest ^= *p_src` побайтно). Прозрачности у него нет вообще —
|
||||
XOR'ится весь прямоугольник, но прозрачные пиксели спрайта это
|
||||
чёрный 0x000000, а XOR с нулём ничего не меняет.
|
||||
|
||||
Отсюда и берётся необходимость СВОЕЙ палитры: XOR двух цветов игровой
|
||||
палитры даёт цвет, которого в ней нет.
|
||||
|
||||
## 2. Единственное место, где запечка отличается от оригинала
|
||||
|
||||
XOR идёт по тому, что УЖЕ на экране, то есть результат зависит от фона.
|
||||
Разбор по пикселям показывает, что зависимость узкая:
|
||||
|
||||
| пиксель | первый проход | второй проход | зависит от фона? |
|
||||
|---|---|---|---|
|
||||
| спрайт непрозрачен в x | закрашен цветом спрайта | XOR с цветом из x−1 | **нет** |
|
||||
| прозрачен в x, непрозрачен в x−1 | фон | фон XOR цвет | **да** |
|
||||
| прозрачен в обоих | фон | фон | нет (не рисуем) |
|
||||
|
||||
То есть от фона зависит только **кайма в один пиксель по левым кромкам
|
||||
силуэта**. На чёрном фоне (а Тень почти всегда на нём — уровень 4 у
|
||||
зеркала, уровень 6, бой на 12-м) `фон XOR цвет == цвет`, и запечка точна.
|
||||
На светлом фоне оригинал подкрасит эту кайму, мы — нет.
|
||||
|
||||
**Это осознанное расхождение, в `impl_diff.md` при реализации.**
|
||||
|
||||
## 3. Замеры на реальных ассетах
|
||||
|
||||
Прогон алгоритма по всем кадрам (`SDLPoP/data/KID`, `SDLPoP/data/GUARD`):
|
||||
|
||||
| набор | кадров | макс. габарит | разных цветов |
|
||||
|---|---:|---|---:|
|
||||
| Кид (chtab_2) | 219 | 56×57 | 44 |
|
||||
| Страж (chtab_5) | 34 | 53×42 | 16 |
|
||||
| **вместе** | **253** | 57×57 (с учётом +1 px сдвига) | **59** |
|
||||
|
||||
Всего пикселей во всех кадрах Тени — 96 746. Квантование до N цветов
|
||||
(оставляем N самых частых, остальные — в ближайший):
|
||||
|
||||
| палитра | затронуто пикселей | доля | худшая ошибка (евклид RGB) |
|
||||
|---|---:|---:|---:|
|
||||
| 16 цветов | 3 483 | 3,60 % | 151 |
|
||||
| **32 цвета** | **426** | **0,44 %** | 113 |
|
||||
|
||||
### Насколько заметна подмена
|
||||
|
||||
«Затронуто» само по себе ничего не говорит — важно, НА СКОЛЬКО сместился
|
||||
цвет. Порог различимости на плоской заливке ~30-40 единиц евклида в RGB
|
||||
(максимум возможного — 441). Пробовал три стратегии подбора:
|
||||
|
||||
- **A** — оставить N самых частых цветов ТОЧНО, остальные в ближайший;
|
||||
- **B** — взвешенный k-means по всем цветам (двигает вообще все);
|
||||
- **C** — гибрид: часть слотов под точные частые, остаток — кластеры хвоста.
|
||||
|
||||
| палитра | стратегия | изменено px | заметно (40..90) | сильно (>90) | худшая | средняя |
|
||||
|---|---|---:|---:|---:|---:|---:|
|
||||
| 16 | A самые частые | 3 483 (3,60 %) | 2 342 | 257 | 151 | 57,7 |
|
||||
| 16 | B k-means | 29 368 (30,4 %) | 1 972 | 256 | 142 | 10,9 |
|
||||
| 16 | C гибрид 10+6 | 7 255 (7,50 %) | 2 146 | **129** | 142 | 33,7 |
|
||||
| **32** | **A самые частые** | **426 (0,44 %)** | **205** | **27** | 113 | 46,0 |
|
||||
| 32 | B k-means | 3 425 (3,54 %) | 133 | 27 | 110 | 9,1 |
|
||||
| 32 | C гибрид 20+12 | 1 997 (2,06 %) | 133 | 27 | 110 | 16,0 |
|
||||
|
||||
Читается так. При 32 цветах стратегия не важна: «сильно» — ровно **27
|
||||
пикселей на все 253 кадра** в любой из трёх, это неустранимые одиночки без
|
||||
близкого соседа (по 1-3 пикселя на кадр, увидеть нельзя). Перцептивно
|
||||
значимого остаётся 232 пикселя из 96 746, то есть примерно один пиксель на
|
||||
кадр. При 16 цветах разница уже есть: гибрид вдвое срезает «сильно» (257
|
||||
→ 129), но заметных всё равно две с лишним тысячи.
|
||||
|
||||
Решение: **32 цвета, стратегия A** — она проще всех и оставляет ВСЕ частые
|
||||
цвета пиксельно точными. Если когда-нибудь понадобится ужаться до 16,
|
||||
брать гибрид C, а не «самые частые».
|
||||
|
||||
## 4. Палитра: что занято и куда класть
|
||||
|
||||
| блок | кто | примечание |
|
||||
|---|---|---|
|
||||
| 0x30..0x3F | VGA-16 | общая; из неё цвет вспышки, пузырьки зелий, отладочная метка |
|
||||
| 0x40..0x4F | chtab_1 | зелья, пламя |
|
||||
| 0x50..0x5F | env тайлсета | **меняется** подземелье/дворец |
|
||||
| 0x60..0x6F | wall тайлсета | **меняется** подземелье/дворец |
|
||||
| 0x70..0x7F | chtab_2 | Кид |
|
||||
| 0x80..0x8F | chtab_0 | меч в руке |
|
||||
| 0x90..0x9F | chtab_5 | страж, перезаливается цветом стража |
|
||||
|
||||
Занято 112 слотов из 256, **свободно 144** — девять выровненных блоков по
|
||||
16. Оговорки: 0xFF в наших атласах это маркер прозрачности, а запись 0
|
||||
правит `flash_bg`, так что блоки 0x00 и 0xF0 лучше не трогать.
|
||||
|
||||
**Берём 0xA0..0xBF** (32 слота) — сразу за стражем, персонажи остаются
|
||||
сгруппированы.
|
||||
|
||||
Важно: в наших атласах прозрачность кодируется байтом 0xFF, а исходный
|
||||
индекс 0 в них означает «прозрачно». У Тени **чёрный — настоящий цвет**
|
||||
(это XOR-погашенная середина силуэта, 51 % всех её пикселей), поэтому у
|
||||
неё маппинг свой: «нет пикселя» → 0xFF, цвет k → 0xA0 + k, и слот 0xA0 =
|
||||
чёрный НЕПРОЗРАЧНЫЙ.
|
||||
|
||||
## 5. Объём
|
||||
|
||||
253 кадра против 219 у Кида — по страницам EMM примерно как нынешний
|
||||
набор Кида (28 страниц), плюс пара на кадры стража. При 215 свободных
|
||||
страницах на старте (memory `sprinter_emm_budget`) это не проблема.
|
||||
|
||||
Кадры смерти («убитый Кид») пока НЕ вырезаем — экономия несколько
|
||||
страниц, а риск промахнуться мимо нужного кадра реальный: Тень на 12-м
|
||||
уровне умирает.
|
||||
|
||||
## 6. План работ
|
||||
|
||||
**Ш1. Упаковщик** `toolchain/pop_pack_shadow.py`: прогнать оба набора
|
||||
через алгоритм §1, собрать 32-цветную палитру, выдать
|
||||
`poc/res/shadow/shadow0..N.atl` + `shadow.pal` + `pop_shadow_atlas.h`.
|
||||
Критерий: предпросмотр PNG совпадает с видом Тени в SDLPoP.
|
||||
|
||||
**Ш2. Загрузка**: `pop_shadow_load()` рядом с `pop_kid_load`, палитра в
|
||||
0xA0..0xBF, и обе страницы дабл-буфера (как `bg_load_tile_pal`).
|
||||
Грузить ЛЕНИВО — только когда на уровне есть Тень (4, 5, 6, 12), иначе
|
||||
28 страниц EMM висят зря.
|
||||
|
||||
**Ш3. Отрисовка**: одно место — `pop_cdraw.c:551..568`, где выбирается
|
||||
`pages`. Сейчас там для соперника берётся `gp`, а для не-боевых кадров
|
||||
Тени подменяется на `kidp`; добавится ветка «charid == CHARID_1_SHADOW →
|
||||
shadowp», причём обе половины набора (кадры Кида и кадры стража) лежат в
|
||||
ОДНОМ атласе Тени, так что подмена становится проще нынешней.
|
||||
|
||||
**Ш4. Проверка в MAME**: уровень 4 (Тень у зеркала), уровень 6 (Тень
|
||||
крадёт зелье), уровень 12 (бой с Тенью — там она в боевых кадрах, то
|
||||
есть проверяется вторая половина набора).
|
||||
|
||||
## 7. Что проверить артефактом до Ш3
|
||||
|
||||
1. Индекс кадра для Тени в боевых кадрах: `frame_tbl_guard` адресуется
|
||||
как `frame + add_frame - 149` (`seg006.c:535`), то есть у нашего
|
||||
атласа Тени нумерация двух половин должна совпадать с тем, что уже
|
||||
делает `pop_frame_tbl_is_guard`.
|
||||
2. Цвет стража для Тени: у не-стражей `curr_guard_color = 0`
|
||||
(`seg002:183`), значит запекать половину «кадры стража» надо с
|
||||
палитрой стража цвета 0, а не с текущей.
|
||||
Reference in New Issue
Block a user