Next Hexagon
29. 4. 2026

V Next Hexagon jsem původně řešil rotující pozadí velmi přímočaře. Pro každou fázi rotace existoval hotový bitmapový obraz pozadí a hra si podle aktuální hodnoty rotace vybrala odpovídající frame. Jeden takový frame měl velikost 48 KB, protože Layer 2 obrazovka v režimu 256 x 192 při 256 barvách zabírá právě 256 × 192 bajtů. Z hlediska jednoduchosti to byl velmi příjemný přístup. Vše bylo předpočítané, během hry se nemusela řešit žádná geometrie, žádné počítání úhlů pro každý pixel a žádné překreslování složitého obrazce. Stačilo mít připravenou sadu obrazů, vybrat správný frame a pomocí DMA ho přenést do Layer 2 bufferu. Nevýhoda ale byla zásadní - pokud má rotace dostatečně jemná, například 32 fází, znamená to 32 × 48 KB, tedy 1536 KB jen pro samotné pozadí. V praxi to znamenalo, že takové řešení bylo použitelné jen na ZX Spectrum Next s 2 MB RAM. Na stroji s 1 MB se tím paměťový rozpočet prakticky rozpadl.

Rotace pozadí

V Next Hexagon jsem původně řešil rotující pozadí velmi přímočaře. Pro každou fázi rotace existoval hotový bitmapový obraz pozadí a hra si podle aktuální hodnoty rotace vybrala odpovídající frame. Jeden takový frame měl velikost 48 KB, protože Layer 2 obrazovka v režimu 256 x 192 při 256 barvách zabírá právě 256 × 192 bajtů. Z hlediska jednoduchosti to byl velmi příjemný přístup. Vše bylo předpočítané, během hry se nemusela řešit žádná geometrie, žádné počítání úhlů pro každý pixel a žádné překreslování složitého obrazce. Stačilo mít připravenou sadu obrazů, vybrat správný frame a pomocí DMA ho přenést do Layer 2 bufferu. Nevýhoda ale byla zásadní - pokud má rotace dostatečně jemná, například 32 fází, znamená to 32 × 48 KB, tedy 1536 KB jen pro samotné pozadí. V praxi to znamenalo, že takové řešení bylo použitelné jen na ZX Spectrum Next s 2 MB RAM. Na stroji s 1 MB se tím paměťový rozpočet prakticky rozpadl.
Nové řešení vychází z jiné myšlenky. Pozadí už není uloženo jako sada hotových barevných obrázků, ale jako jedna statická fázová bitmapa. V té nejsou pixely pozadí chápány primárně jako finální barvy, ale jako indexy fáze. Konkrétně se používá rozsah paletových indexů $80 až $9F, tedy 32 různých hodnot. Každý pixel tak v sobě nese informaci, do které úhlové fáze patří. Samotná rotace se potom nedělá změnou bitmapy, ale změnou významu těchto 32 paletových položek. Jinými slovy - data v Layer 2 zůstávají stejná, ale přepíše se paleta tak, aby se fáze posunuly. Pixel s indexem $80 nemusí pokaždé znamenat stejnou barvu. Podle aktuální rotace může být zařazen do první nebo druhé sady pruhů, a tím vznikne vizuální dojem, že se celé pozadí otáčí.

Princip je jednoduchý, ale velmi účinný. Pro každý pixel s indexem $80 + phase se spočítá logická fáze jako (phase + rotation) & 31. Pokud tato logická fáze spadá do rozsahu 0 až 15, dostane barvu prvního pruhu. Pokud spadá do rozsahu 16 až 31, dostane barvu druhého pruhu. Tím se z 32 fázových indexů vytvoří rotující vzor dvou barevných oblastí. Protože se mění jen mapování indexů na barvy, není potřeba mít v paměti 32 hotových obrazovek. Stačí jedna 48KB bitmapa, která obsahuje fázové indexy, a k tomu malá rutina, která přepíše 32 položek Layer 2 palety. Na myšlenku použít rotaci pomocí palety mě nasměroval Martin Bórik, a právě tento nápad zásadně změnil paměťovou náročnost celého efektu.

Rozdíl v paměti je obrovský. Staré řešení potřebovalo pro 32 fází přibližně 1536 KB dat, protože každá fáze byla samostatný 48KB bitmapový frame. Nové řešení potřebuje pro vlastní pozadí pouze 48 KB, protože existuje jen jedna statická fázová bitmapa. K tomu se přidává zanedbatelné množství dat pro paletovou tabulku a drobný runtime stav, například aktuální barevná fáze, poslední aplikovaná rotace a poslední aplikovaná fáze barevného cyklu. Z pohledu paměti se tedy hlavní část efektu zmenšila z velikosti přesahující jeden a půl megabajtu na jednu obrazovku Layer 2.

screen1
Z hlediska CPU práce je důležité rozlišit dvě různé části problému. První je naplnění Layer 2 bufferu bitmapou pozadí. To se stále může dělat pomocí DMA, protože statická fázová bitmapa má pořád 48 KB. Při plném refreshi se tedy do back bufferu přenese jedna kompletní obrazovka. To je podobné jako u starého řešení, jen se nekopíruje jiný frame pro každou rotaci, ale pořád stejný fázový obraz. Druhá část je samotná rotace. A tady je zásadní rozdíl. Ve starém systému rotace znamenala výběr a přenos jiné 48KB bitmapy. V novém systému rotace znamená přepsat 32 paletových položek. To je z hlediska CPU i paměťové propustnosti úplně jiná liga. Místo aby se kvůli vizuálnímu posunu pracovalo s desítkami kilobajtů obrazových dat, pracuje se jen s několika desítkami hodnot v paletě.

Další výhoda nového řešení je, že rotace je oddělená od samotných obrazových dat. Fázová bitmapa říká, jaký úhlový charakter má daný pixel, zatímco paleta říká, jak se tato fáze aktuálně zobrazí. Díky tomu se dá současně zachovat i pomalý barevný cyklus. V Next Hexagon se neřeší jen posun pruhů, ale také změna barev v čase. Paletová rutina proto nepřepisuje položky $80 až $9F pouze podle rotace, ale také podle aktuální barevné fáze. V tabulce je připraveno několik dvojic barev pro pruh A a pruh B a runtime si podle aktuální fáze vybere dvojici, kterou následně aplikuje na všech 32 fázových položek. Výsledek je, že pozadí se může otáčet a zároveň barevně pulzovat, aniž by bylo potřeba držet v paměti další sady bitmap.

Implementačně je zajímavé i to, že paleta se nepřepisuje zbytečně. Rutina si pamatuje poslední použitou hodnotu rotace a poslední použitou barevnou fázi. Pokud se nezměnila ani rotace modulo 32, ani barevná fáze, není nutné dělat vůbec nic. To je důležité hlavně proto, že herní rotace v Next Hexagon interně pracuje s jemnějším rozsahem 0 až 95, zatímco pozadí používá 32 fází. Ne každá změna herní rotace tedy musí nutně znamenat jiný vzhled fázového pozadí. Paletová rutina si vezme rotation & 31 a teprve podle této hodnoty rozhodne, jestli je potřeba přegenerovat mapování položek $80 až $9F. Tím se šetří CPU čas a zároveň se zbytečně neposílají hodnoty do Next registrů.
screen2
Staré bitmapové řešení mělo pořád určité výhody. Bylo velmi přímočaré, vizuálně přesné a každý frame mohl být ručně nebo automaticky vyrenderovaný přesně tak, jak měl vypadat. Pokud by pozadí obsahovalo složité barevné přechody, textury nebo prvky, které nejdou snadno vyjádřit jen změnou palety, sada hotových framů by nabízela větší volnost. U rotujícího hexagonového pozadí je ale struktura efektu pravidelná a velmi dobře se dá vyjádřit právě fází. V takovém případě je ukládání hotových barevných framů zbytečně drahé. Bitmapa nepotřebuje nést informaci „jaká je tady barva v této konkrétní rotaci“, stačí aby nesla informaci „jaká je tady fáze“. Barva se pak dopočítá nepřímo přes paletu.

V Next Hexagon se navíc celé řešení dobře doplňuje s double bufferingem a obnovou špinavých oblastí. Při přechodu do hry nebo při potřebě plného obnovení se statická 48KB fázová bitmapa přenese do back bufferu. Během běžného gameplaye ale není nutné pořád obnovovat celou obrazovku. Rutina obnovuje jen oblasti, které se opravdu zašpiní kreslením do Layer 2, například středový pás s tmavým hexagonem a oblast HUDu s časem. Hardwarové sprity hráče a ringů Layer 2 přímo nešpiní, takže není nutné kvůli nim překopírovávat celé pozadí. To je další důležitý rozdíl proti naivnímu přístupu, kde by se každý frame znovu posílala celá obrazovka jen proto, aby se vizuálně změnila rotace.
screen3
Jak vznikla statická 48KB fázová bitmapa
Statická 48KB fázová bitmapa nevznikla ručním kreslením nového pozadí, ale převodem původní předpočítané animace do úspornější reprezentace. Původní verze už obsahovala 32 hotových framů rotujícího pozadí. Každý frame měl velikost 48 KB, protože šlo o kompletní Layer 2 obrazovku 256 x 192 pixelů. Tyto framy tedy obsahovaly přesnou informaci o tom, jak měl každý pixel vypadat při každém kroku rotace. Nový generátor tuto informaci využil opačným směrem. Nepotřeboval znovu vymýšlet tvar pozadí, ale z existujících framů zpětně odvodil, do které rotační fáze patří každý pixel.

Výsledkem tohoto převodu je jedna statická bitmapa o velikosti 49152 bajtů, tedy 48 KB. Důležité ale je, že tyto bajty už neznamenají finální barvu pixelu. U běžného Layer 2 obrázku je každý bajt index do palety, který říká, jakou barvou se pixel zobrazí. V tomto případě je hodnota pixelu použitá jako číslo fáze. Generátor pro jednotlivé pixely ukládá hodnoty v rozsahu $80$9F, což odpovídá 32 možným fázím. Hodnota $80 znamená fázi 0, hodnota $81 fázi 1 a tak dále až po $9F, která znamená fázi 31. Samotná bitmapa tedy neříká „tady má být tato barva“, ale spíš „tento pixel patří do této fáze rotace“.

Převod provádí skript v pythonu. Skript načte starý bankovaný soubor s 32 plnými framy pozadí a u každého pixelu se podívá, jakou hodnotu měl tento pixel ve všech 32 rotačních krocích. Potom si vytvoří 32 ideálních průběhů. Každý ideální průběh odpovídá jedné možné fázi a říká, jestli by se pixel při jednotlivých rotacích zobrazil jako barva A, nebo jako barva B. Tato simulace používá stejný princip jako runtime paletová rotace ve hře: pro fázi phase a rotaci rot se spočítá výraz (phase + rot) & 31. Pokud je výsledek menší než 16, použije se barva A. Pokud je výsledek 16 až 31, použije se barva B.

Generátor tedy pro každý pixel zkouší všech 32 možných fází a porovnává je s původními 32 framy. Pro fázi 0 spočítá, kolikrát by se ideální paletové chování lišilo od původních dat. Potom totéž udělá pro fázi 1, fázi 2 a tak dál až po fázi 31. Fáze s nejmenším počtem rozdílů se vybere jako nejlepší vysvětlení chování daného pixelu. Pokud některá fáze odpovídá přesně, není potřeba zkoušet dál a pixel dostane tuto fázi. Do výsledné bitmapy se pak nezapíše původní barva, ale hodnota $80 + best_phase. Tím se z mnoha hotových obrázků stane jedna mapa fázového posunu.

Tento postup je výhodný právě proto, že původní animace obsahovala velké množství opakující se informace. Většina pixelů se při rotaci nechovala náhodně. Jejich barva se měnila pravidelně podle toho, jak přes dané místo procházel rotující pruh. Generátor tuto pravidelnost zachytil jako číslo fáze. Všechno, co se dříve muselo ukládat jako 32 samostatných 48KB obrazovek, se tím převedlo do jedné 48KB obrazovky. Ta už není sadou hotových barev pro jeden konkrétní okamžik, ale prostorovou mapou rotačních fází. Runtime paleta potom při každé rotaci pouze přemapuje položky $80$9F na aktuální dvojici barev.

Ve vygenerovaném souboru se navíc ukládá i statistika přesnosti převodu. Skript si vede histogram chyb a při zápisu výsledného assemblerového souboru vypíše, kolik pixelů odpovídalo původním framům přesně a jaká byla průměrná chyba. To je užitečné při kontrole, jestli převod opravdu vystihuje původní animaci. Pokud by se pozadí změnilo, stačí znovu spustit generátor nad novými 32 framy a ověřit, jestli se výsledná fázová bitmapa stále dobře shoduje s původními daty.

Samostatně je řešený středový tmavý hexagon. Ten se nechová stejně jako zbytek fázového pozadí, protože jde o překryvný tvar uprostřed obrazovky. Skript proto umí kromě samotné fázové bitmapy vygenerovat také scanline spany pro středový hexagon. Pro každý z 32 původních framů projde vymezenou středovou oblast, najde pixely se středovou barvou a převede je na úsporný seznam vodorovných úseků. Každý úsek má tvar y, x_start, length. Ve hře se potom podle aktuální rotace vybere odpovídající sada spanů a středový hexagon se překreslí do Layer 2 bufferu zvlášť. Velká plocha pozadí je tedy řešená paletovou fázovou bitmapou, zatímco středový hexagon zůstává samostatný překryvný prvek.

Takže ve zkratce: původních 32 framů nebylo zahozeno hned. Nejdřív posloužily jako referenční data pro generátor. Ten pro každý pixel našel nejlepší fázový index 0 až 31, uložil ho jako hodnotu $80$9F a tím vytvořil jedinou 48KB Layer 2 bitmapu. Runtime už potom nepotřebuje původních 32 obrázků. Stačí mu tato mapa fází a malá paletová rutina, která hodnotám $80$9F přiřadí nové barvy podle aktuální rotace. Tím se původní předpočítaná animace převedla z mnoha hotových obrazovek na jeden statický obraz s uloženým fázovým posunem.
Zde se můžete podívat na screenshoty aktuálního stavu hry:

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *