267 lines
18 KiB
Markdown
267 lines
18 KiB
Markdown
# Атлас Тени — разбор и план
|
||
|
||
Дата: 2026-08-20. Статус: **ЗАКРЫТО.** Ш0-Ш4 сделаны, прогон
|
||
пользователем на уровнях **4, 5, 6 и 12** — всё корректно.
|
||
|
||
Задача: сейчас Тень рисуется спрайтами Кида (вне боя) и спрайтами стража
|
||
(в бою) как есть, поэтому от Кида она не отличается. В оригинале её вид
|
||
даёт наложение спрайта на себя со сдвигом; наш пакетный блит такого не
|
||
умеет, поэтому запекаем результат в отдельный атлас заранее.
|
||
|
||
## 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 двух цветов игровой
|
||
палитры даёт цвет, которого в ней нет.
|
||
|
||
### Каким набором спрайтов рисуется Тень (проверено 2026-08-20)
|
||
|
||
Сначала я решил, что в боевых кадрах Тень рисуется спрайтами СТРАЖА, и
|
||
записал это в план. **Это было неверно, поправка ниже.**
|
||
|
||
Набор выбирает НЕ charid. `load_frame_to_obj` (`seg008.c:1752`):
|
||
|
||
```c
|
||
word chtab_base = id_chtab_2_kid; // жёстко Кид
|
||
obj_chtab = chtab_base + (cur_frame.sword >> 6); // старшие 2 бита кадра
|
||
```
|
||
|
||
то есть набор берётся из САМИХ ДАННЫХ КАДРА, поле `sword`: младшие 6 бит —
|
||
картинка меча, старшие два — смещение chtab относительно Кида
|
||
(`types.h:361`). Проверил обе таблицы:
|
||
|
||
- `frame_table_kid` — **все 241 кадра** имеют `sword & 0xC0 == 0` → chtab_2.
|
||
Значит **Кид всегда рисуется своими спрайтами**, боевые кадры 150..189 не
|
||
исключение;
|
||
- `frame_tbl_guard` — все кадры имеют `0xC0` → chtab_5.
|
||
|
||
Тень берёт `frame_tbl_guard` для кадров 150..189 (`seg006.c:533`), значит в
|
||
бою она идёт через chtab_5. **Но chtab_5 — это не «страж», это «соперник
|
||
уровня»**: он грузится из `tbl_guard_dat[tbl_guard_type[уровень]]`
|
||
(`seg000.c:1117`), а `tbl_guard_type[12] == 4` → **SHADOW.DAT**.
|
||
|
||
Открыл SHADOW.DAT: его палитра **побайтно равна палитре Кида** (у GUARD.DAT
|
||
там серая рампа под перекраску), а спрайты — Кид в боевых позах, не страж.
|
||
Отрендерил тройками «Кид / SHADOW.DAT / GUARD.DAT» для кадров
|
||
151/153/158/161/167: первые две колонки — один и тот же персонаж, третья —
|
||
серый страж в тюрбане.
|
||
|
||
**Вывод: Тень ВСЕГДА выглядит Кидом, и ощущение пользователя верно.**
|
||
Спрайты при этом лежат в двух файлах: не-боевые кадры в chtab_2 (KID),
|
||
боевые — в chtab_5, заполненном SHADOW.DAT.
|
||
|
||
### Можно ли взять для боя собственные кадры Кида
|
||
|
||
Механически да: `frame_table_kid` покрывает 150..189 со своей геометрией,
|
||
и хватило бы снять спецветку для `charid_1_shadow` в `load_frame`. Но
|
||
кадры НЕ совпадают: из 34 боевых у 31 отличается габарит (на 1-6 px), у 3
|
||
отличаются пиксели. Это разная графика, а не одна и та же в двух файлах.
|
||
Поэтому берём SHADOW.DAT — он и есть «кадры Кида для Тени», подготовленные
|
||
авторами.
|
||
|
||
## 2. Единственное место, где запечка отличается от оригинала
|
||
|
||
XOR идёт по тому, что УЖЕ на экране, то есть результат зависит от фона.
|
||
Разбор по пикселям показывает, что зависимость узкая:
|
||
|
||
| пиксель | первый проход | второй проход | зависит от фона? |
|
||
|---|---|---|---|
|
||
| спрайт непрозрачен в x | закрашен цветом спрайта | XOR с цветом из x−1 | **нет** |
|
||
| прозрачен в x, непрозрачен в x−1 | фон | фон XOR цвет | **да** |
|
||
| прозрачен в обоих | фон | фон | нет (не рисуем) |
|
||
|
||
То есть от фона зависит только **кайма в один пиксель по левым кромкам
|
||
силуэта**. На чёрном фоне (а Тень почти всегда на нём — уровень 4 у
|
||
зеркала, уровень 6, бой на 12-м) `фон XOR цвет == цвет`, и запечка точна.
|
||
На светлом фоне оригинал подкрасит эту кайму, мы — нет.
|
||
|
||
**Это осознанное расхождение, в `impl_diff.md` при реализации.**
|
||
|
||
## 3. Замеры на реальных ассетах
|
||
|
||
Прогон алгоритма по правильным наборам (`SDLPoP/data/KID` +
|
||
`SDLPoP/data/SHADOW`):
|
||
|
||
| набор | кадров | макс. габарит | разных цветов |
|
||
|---|---:|---|---:|
|
||
| Кид (chtab_2) | 219 | 56×57 | 44 |
|
||
| SHADOW.DAT (chtab_5 на ур. 12) | 32 | 49×38 | 22 |
|
||
| **вместе** | **251** | 57×57 (с учётом +1 px сдвига) | **45** |
|
||
|
||
Цветов вместе почти столько же, сколько у одного Кида: SHADOW.DAT сидит на
|
||
той же палитре, новых сочетаний XOR почти не даёт. Всего пикселей во всех
|
||
кадрах Тени — 91 045.
|
||
|
||
### Насколько заметна подмена
|
||
|
||
«Затронуто» само по себе ничего не говорит — важно, НА СКОЛЬКО сместился
|
||
цвет. Порог различимости на плоской заливке ~30-40 единиц евклида в RGB
|
||
(максимум возможного — 441). Пробовал три стратегии подбора:
|
||
|
||
- **A** — оставить N самых частых цветов ТОЧНО, остальные в ближайший;
|
||
- **B** — взвешенный k-means по всем цветам (двигает вообще все);
|
||
- **C** — гибрид: часть слотов под точные частые, остаток — кластеры хвоста.
|
||
|
||
| палитра | стратегия | изменено px | заметно (40..90) | сильно (>90) | худшая |
|
||
|---|---|---:|---:|---:|---:|
|
||
| 16 | A самые частые | 959 (1,05 %) | 569 | 381 | 128 |
|
||
| **16** | **C гибрид 5+11** | 6 984 (7,67 %) | 1 078 | **76** | 114 |
|
||
| 32 | A самые частые | 50 (0,05 %) | 40 | 4 | 113 |
|
||
| 32 | C гибрид 28+4 | 73 (0,08 %) | 58 | **0** | 90 |
|
||
|
||
Читается так. При 32 цветах всё практически идеально: 73 пикселя на 251
|
||
кадр, грубых промахов нет вовсе. При 16 цветах выбор стратегии виден:
|
||
«самые частые» трогает меньше пикселей (959), но 381 из них уезжает СИЛЬНО;
|
||
гибрид размазывает ошибку — грубых остаётся 76, то есть примерно **0,3
|
||
пикселя на кадр**.
|
||
|
||
### Решение: **16 цветов, стратегия C (гибрид 5 + 11)**
|
||
|
||
Числа выше — про пиксели, а решает глаз. Отрендерил одни и те же кадры в
|
||
трёх видах (точный цвет / 32 / 16) и сравнил в увеличении ×3: **отличий
|
||
не видно**. Объяснение в самих числах: перцептивно значимых пикселей при
|
||
16 цветах гибридом — 1 154 на 251 кадр, это ~4,6 пикселя на кадр при
|
||
~1 000 видимых, и они РАССЫПАНЫ по контуру, а не собраны в пятно.
|
||
|
||
Поэтому берём 16, а не 32: экономим блок палитры (пригодится под будущие
|
||
наборы — принцесса, визирь, мышь), а разница неразличима.
|
||
|
||
**Но стратегия обязана быть гибридной.** При 16 цветах «взять самые
|
||
частые» впятеро хуже по грубым промахам (381 пиксель против 76), и это
|
||
единственное место, где выбор стратегии виден. Гибрид: 5 самых частых
|
||
берём ТОЧНО, оставшиеся 11 слотов отдаём под взвешенные кластеры хвоста.
|
||
|
||
Если в реальной игре кайма всё же будет резать глаз — переход на 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. Объём
|
||
|
||
251 кадр против 219 у Кида — по страницам EMM примерно как нынешний
|
||
набор Кида (28 страниц), плюс пара на кадры из SHADOW.DAT. При 215 свободных
|
||
страницах на старте (memory `sprinter_emm_budget`) это не проблема.
|
||
|
||
Кадры смерти («убитый Кид») пока НЕ вырезаем — экономия несколько
|
||
страниц, а риск промахнуться мимо нужного кадра реальный: Тень на 12-м
|
||
уровне умирает.
|
||
|
||
## 6. План работ
|
||
|
||
**Ш0. Довезти SHADOW.DAT.** Его у нас нет вовсе (см. BUG-SHADOW-SET) —
|
||
добавить каталог ассетов и правило в Makefile рядом с GUARD/SKEL/VIZIER.
|
||
|
||
**Ш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»
|
||
БЕЗ подмены: обе половины (кадры Кида и кадры SHADOW.DAT) лежат в ОДНОМ
|
||
атласе Тени, так что ветка становится проще нынешней.
|
||
|
||
**Ш4. Проверка в MAME**: уровень 4 (Тень у зеркала), уровень 6 (Тень
|
||
крадёт зелье), уровень 12 (бой с Тенью — там она в боевых кадрах, то
|
||
есть проверяется вторая половина набора).
|
||
|
||
## 7. Что проверить артефактом до Ш3
|
||
|
||
1. Индекс кадра для Тени в боевых кадрах: `frame_tbl_guard` адресуется
|
||
как `frame + add_frame - 149` (`seg006.c:535`), то есть у нашего
|
||
атласа Тени нумерация двух половин должна совпадать с тем, что уже
|
||
делает `pop_frame_tbl_is_guard`.
|
||
2. Перекраска Тени НЕ нужна: SHADOW.DAT приходит уже в палитре Кида, а
|
||
`curr_guard_color` у не-стражей равен 0 (`seg002:183`). Ветку
|
||
`pop_guard_set_palette` для типа 4 звать нельзя — она затрёт палитру
|
||
Тени палитрой стража.
|
||
|
||
---
|
||
|
||
# 8. Как сделали (2026-08-20)
|
||
|
||
- `toolchain/pop_pack_shadow.py` — запекает обе половины, собирает
|
||
16-цветную палитру гибридом (получилось 5 частых точно + 11 кластеров
|
||
хвоста), пишет `poc/res/shadow/sk0..27.atl`, `sf0..3.atl` и
|
||
`roomtest/pop_shadow_atlas.h`. Итог: **251 спрайт, 32 EMM-страницы,
|
||
225 178 Б**.
|
||
- `roomtest/pop_shadow.c/.h` (банк 8) — загрузка. **Грузим один раз при
|
||
старте**, а не по уровням: страниц EMM с запасом, а забыть перезагрузку
|
||
на границе легко — ровно так и появился BUG-SHADOW-SET.
|
||
- `pop_cdraw.c` — одна ветка выбора атласа; заодно брызги урона теперь
|
||
берут атлас отдельным указателем (`spl`), потому что у оригинала они
|
||
всегда из «своего» chtab, а у Тени кадр может прийти из другой
|
||
половины набора.
|
||
|
||
## Грабля, стоившая одного прогона
|
||
|
||
Набор загрузился, силуэт нарисовался правильной формы — и **целиком
|
||
чёрный**. Причина: `gfx_pal_fload("KID\\kid.pal")` заливает ВСЕ 256
|
||
записей палитры и затирает любые слоты, выставленные до него. Ровно та
|
||
же беда уже была с тайлсетом — сразу за этим вызовом стоит
|
||
`pop_bg_pal_apply`. Поэтому палитра Тени вынесена в отдельный
|
||
`pop_shadow_pal_apply()` и зовётся там же, а не внутри загрузки атласов.
|
||
|
||
**Правило на будущее: любой новый набор палитровых слотов красится ПОСЛЕ
|
||
kid.pal, рядом с pop_bg_pal_apply.**
|
||
|
||
## Проверено
|
||
|
||
Уровни 4 (рождение из зеркала), 5 (кража зелья), 6 (прыжок через
|
||
пропасть) и 12 (бой — там работает вторая половина набора, `sf*`) —
|
||
прогон пользователя 2026-08-20, расхождений не найдено.
|
||
|
||
Расхождение из §2 (кайма в один пиксель по левым кромкам на НЕчёрном
|
||
фоне) записано в `impl_diff.md`.
|