From acb483897d2492682392a9e0134961519a27e5c1 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 20 Aug 2026 10:38:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20=D0=B0?= =?UTF-8?q?=D1=82=D0=BB=D0=B0=D1=81=D0=B0=20=D0=A2=D0=B5=D0=BD=D0=B8:=20?= =?UTF-8?q?=D0=B0=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=20=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B3=D0=B8=D0=BD=D0=B0=D0=BB=D0=B0,=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D1=80=D1=8B=20=D0=BF=D0=B0=D0=BB=D0=B8=D1=82=D1=80?= =?UTF-8?q?=D1=8B,=20=D0=BF=D0=BB=D0=B0=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XOR у оригинала идёт по 24-битному RGB, а blitters_2_or — обычный блит с colour key 0. От фона зависит только кайма в один пиксель по левым кромкам силуэта; на чёрном фоне запечка точна. Замеры: 253 кадра (Кид 219 + страж 34, Тень в боевых кадрах рисуется спрайтами СТРАЖА), 59 разных цветов. 32 цвета оставляют перцептивно значимыми 232 пикселя из 96 746. Палитра: занято 112 слотов, свободно 144; берём 0xA0..0xBF. --- applications/PoP/docs/shadow_atlas_plan.md | 167 +++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 applications/PoP/docs/shadow_atlas_plan.md diff --git a/applications/PoP/docs/shadow_atlas_plan.md b/applications/PoP/docs/shadow_atlas_plan.md new file mode 100644 index 0000000..5bbd3be --- /dev/null +++ b/applications/PoP/docs/shadow_atlas_plan.md @@ -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, а не с текущей.