185 lines
13 KiB
Markdown
185 lines
13 KiB
Markdown
# Атлас Тени — разбор и план
|
||
|
||
Дата: 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), но заметных всё равно две с лишним тысячи.
|
||
|
||
### Решение: **16 цветов, стратегия C (гибрид 10 + 6)**
|
||
|
||
Числа выше — про пиксели, а решает глаз. Отрендерил одни и те же кадры в
|
||
трёх видах (точный цвет / 32 / 16) и сравнил в увеличении ×3: **отличий
|
||
не видно**. Объяснение в самих числах: перцептивно значимых пикселей при
|
||
16 цветах 2 275 на 253 кадра, это ~9 пикселей на кадр при ~1 000 видимых,
|
||
и они РАССЫПАНЫ по контуру, а не собраны в пятно.
|
||
|
||
Поэтому берём 16, а не 32: экономим блок палитры (пригодится под будущие
|
||
наборы — принцесса, визирь, мышь), а разница неразличима.
|
||
|
||
**Но стратегия обязана быть гибридной.** При 16 цветах «взять самые
|
||
частые» вдвое хуже по грубым промахам (257 пикселей против 129), и это
|
||
единственное место, где выбор стратегии виден. Гибрид: 10 самых частых
|
||
берём ТОЧНО, оставшиеся 6 слотов отдаём под взвешенные кластеры хвоста.
|
||
|
||
Если в реальной игре кайма всё же будет резать глаз — переход на 32 цвета
|
||
это одна константа в упаковщике и один блок палитры, данные не меняются.
|
||
|
||
## 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..0xAF** (16 слотов) — сразу за стражем, персонажи остаются
|
||
сгруппированы, а блок 0xB0 остаётся свободным (под 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, собрать 16-цветную палитру гибридом (10 точных + 6
|
||
кластеров хвоста), выдать
|
||
`poc/res/shadow/shadow0..N.atl` + `shadow.pal` + `pop_shadow_atlas.h`.
|
||
Критерий: предпросмотр PNG совпадает с видом Тени в SDLPoP.
|
||
|
||
**Ш2. Загрузка**: `pop_shadow_load()` рядом с `pop_kid_load`, палитра в
|
||
0xA0..0xAF, и обе страницы дабл-буфера (как `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, а не с текущей.
|