Silnice na ZX Spectrum Next
ZX Spectrum Next

Silnice, která se nekreslí - 1. díl nového seriálu

Jak nakreslit pseudo-3D trať se zatáčkami a kopci na počítači, který sotva stihne vyplnit obrazovku pixel po pixelu. Krátká odpověď: skoro nic nekreslit. Silnici složit z hotových řádků, ty nechat kopírovat DMA a zbytek přenechat copperu. Abych Vám vysvětlil, o čem je tento článek a proč ho píši. Procházím si tak adresář s mými rozpracovanými projekty a narazil jsem na složku <cenzored> a v ní něco, co mi přišlo škoda zahodit. Nevím jestli to dotáhnu do konce a proto jsem se rozhodil, že se podělím o své moudra (ehm... já vím), jak jsem minimálně dosáhl toho, čeho se týká tento článek.

Rozpočet: 312 řádků

Next běží na 28 MHz, ale obrazovka 256×192 bodů s bajtem na bod je 48 KB. Kdyby procesor každý snímek zapsal všechny body silnice, i ta nejrychlejší smyčka by zabrala několik snímků. Jeden snímek při 50 Hz je 312 rastrových řádků a na každý připadá 1 792 taktů procesoru. V tomhle rozpočtu se musí vejít fyzika, projekce trati, kreslení silnice, auta, objekty u cesty i zvuk.

Práci je proto potřeba rozdělit mezi čtyři části hardwaru, které Next má navíc oproti původnímu Spectru:

  • Layer 2: obrazová vrstva 256×192 s jedním bajtem na bod a vlastní paletou 256 barev. Na ní je silnice (řádky 0 až 167).
  • zxnDMA: řadič, který kopíruje paměť bez účasti procesoru.
  • Copper: malý koprocesor, který na zvoleném rastrovém řádku zapíše hodnotu do registru Nextu. Program mu sestavíme jednou za snímek a on ho pak sám provádí každý snímek dokola.
  • Tilemapa: vrstva dlaždic 8×8, na kterou se hodí obloha, panorama hor a ovládací panel dole.

Trať jako seznam segmentů

Trať není 3D model. Je to jednorozměrná páska rozdělená na segmenty dlouhé 256 světových jednotek. Silnice má běžně čtyři pruhy a šířku 768 jednotek; krátká zúžení mají tři pruhy a šířku 576. Auto je široké 150 jednotek. Každý segment je popsaný osmi bajty:

BajtObsahVýznam
0zakřiveníznaménkový bajt: o kolik se v tomto segmentu stáčí směr (záporné doleva)
1–2výška16bitová výška silnice na začátku segmentu
3–4objekt Atyp (palma, smrk, cedule…) a stranové odsazení v násobcích 16 jednotek
5–6objekt Bdruhý objekt, typicky menší (keř, kámen)
7šířka a příznakbity 1–7: poloviční šířka v násobcích 4 jednotek; bit 0: start a cíl

Když trať omezíte na 1 024 segmentů, vejde se do 8 KB, což je přesně jedna stránka paměti Nextu. Během jízdy je trať namapovaná do adresního prostoru a ukazatel po ní jen klouže. Kolo dlouhé 1 024 segmentů měří asi 260 000 jednotek, to je při rozumném měřítku několik kilometrů.

Data se nejlépe vyrábějí skriptem (třeba v Pythonu) z popisu po úsecích: 120 segmentů rovně, 60 segmentů ostrá pravá do kopce. Zakřivení i stoupání každého úseku najíždí a dojíždí po křivce ease-in-out, aby zatáčka nezačala skokem. Přes velké kopce se navíc přičte zvlnění terénu ze dvou sinusovek s různou periodou. Poslední krok uzavře smyčku: výškový rozdíl mezi koncem a začátkem kola se plynule rozpustí po celé délce, takže na cílové čáře nevznikne schod.

Proč ne souřadnice X a ZZatáčka uložená jako změna směru, ne jako poloha, dává tu správnou pseudo-3D vlastnost: silnice se v dálce „ohýbá“, ale auto po ní jede pořád rovně dopředu. Zatáčku pak řidič cítí jen jako odstředivou sílu, která ho tlačí ven.

Rasterizace odpředu dozadu

Nejprve se hranice segmentů promítnou do souřadnic obrazovky. Mezi dvěma sousedními promítnutými hranicemi se pak střed silnice a její poloviční šířka na každém řádku dopočítají lineárně. Sklon se spočítá jednou na segment v pevné řádové čárce (10 bitů celé části, 6 bitů zlomku), takže na řádek zbývají jen dvě sčítání.

Segmenty se zpracovávají od nejbližšího k nejvzdálenějšímu a pamatuje se nejvyšší dosud nakreslený řádek. Tomu se říká clip. Vzdálenější segment smí kreslit jen nad clipem, takže každý řádek obrazovky se zpracuje nejvýš jednou. Kopce tím dostávají zakrývání zadarmo: silnice za horizontem kopce se promítne pod clip a prostě se nenakreslí.

Řádek obrazovky je okno do hotového řádku

Silnice se nikdy nekreslí po bodech. V paměti leží dvě sady po 248 předpočítaných řádcích, pro čtyři a tři pruhy. Každý řádek má 1 024 bajtů. Řádek číslo r obsahuje silnici s poloviční šířkou přesně r bodů vycentrovanou na sloupec 512: asfalt, stopy pneumatik, dělicí čáry, bílou krajnici, červenobílý obrubník, pruh štěrku a trávu až do krajů. Celkem 496 KB. Šířka vozovky se v nájezdu i výjezdu mění pozvolna přes 24 segmentů.

Řádek obrazovky je pak jen 256bajtové okno do jednoho z těchto řádků. Číslo řádku určuje šířku silnice v dané vzdálenosti. Začátek okna určuje, kde silnice na obrazovce leží: posunem okna doprava silnice uhne doleva.

okno: 256 bajtů = 1 řádek obrazovky 0 512 (střed silnice) 1023 C C + 255
Jeden z 248 čtyřpruhových řádků (zde poloviční šířka asi 70 bodů, v měřítku 1:1,6). Řádek obrazovky ukazuje 256 bajtů od začátku C.

Vykreslení řádku je tak kopie 256 bajtů a tu udělá DMA. Řadič je předem nastavený v režimu „paměť do paměti, obě adresy rostou, 256 bajtů, burst“, takže pro každý řádek stačí poslat novou zdrojovou a cílovou adresu a příkazy LOAD a ENABLE. Je to osm zápisů na port místo smyčky přes 256 bajtů.

Mlha je v datech

Šířka řádku jednoznačně určuje vzdálenost, ze které pochází. Řádek proto rovnou nese i barvu mlhy: vzdálené (úzké) řádky používají barvy smíchané s barvou oparu. Paleta Layer 2 má 15 úrovní mlhy po 8 barvách (tráva, štěrk, obrubník, krajnice, asfalt, stopy, čára). Silnice tak v dálce plynule bledne a procesor s tím nemá žádnou práci.

Kruhová okna a X-scroll na každém řádku

Když auto zatáčí, posune se silnice na většině řádků o pár bodů. Znovu kopírovat 256 bajtů pro každý řádek by bylo zbytečné, protože se změnilo jen pár bajtů na kraji.

Každý řádek Layer 2 se proto chová jako kruhový buffer. Pro každý řádek si pamatujeme, který předpočítaný řádek a které okno v něm drží, a o kolik je obsah v bufferu „pootočený“. Copper pak pro ten řádek nastaví vodorovný posun Layer 2 (registr $16), takže obrazovka začíná zobrazovat od správného bajtu. Když se silnice posune o s bodů, stačí zkopírovat s nově odkrytých bajtů na místo těch, které odjely z obrazu. Do 32 bajtů to zvládne rychleji procesor instrukcí LDIR, delší úseky jdou přes DMA.

buffer řádku (256 bajtů) nové obsah z minulého snímku ROT: odtud se zobrazuje (X-scroll) obrazovka bajty ROT … 255 0 … ROT−1
Posun silnice o s bodů = zkopírovat s bajtů a změnit X-scroll toho řádku. Platí to i pro jediný bod: obě obrazové paměti musí ukázat stejnou polohu.

Layer 2 je dvakrát (dva buffery po 48 KB). Do jednoho se kreslí, druhý je vidět, a vyměňují se v zatemnění mezi snímky. Stav oken se proto vede pro každý buffer zvlášť.

Pruhy, které nejsou v datech

Pseudo-3D silnice potřebuje střídání světlých a tmavých úseků, jinak by pohyb dopředu nebyl vidět. V předpočítaných řádcích ale žádné pruhy nejsou. Paleta Layer 2 má 256 barev: dolní polovina obsahuje světlé varianty barev silnice, horní polovina tmavé (tmavší asfalt, obrubník prohozený na bílo-červený, bez dělicí čáry). Layer 2 umí ke každému bodu přičíst posun palety a copper ho na začátku každého tmavého úseku přepne na 8 a na začátku světlého zpátky na 0. Pruhy tak nestojí jediný zapsaný bajt pixelů.

Copper: program na jeden snímek

Všechno, co se mění po řádcích, dělá copper. Každý snímek mu sestavíme program z instrukcí WAIT (počkej na řádek) a MOVE (zapiš do registru) a při zatemnění ho nahraje. Zkrácený příklad:

; obloha: přechod barvy pozadí po krocích
WAIT 0     MOVE $4A ← modrá 1        ; barva, kde jsou všechny vrstvy průhledné
WAIT 9     MOVE $4A ← modrá 2
…
WAIT 47    MOVE $30 ← posun hor      ; druhý paralaxní pás panoramatu
; silnice: okno a pruh každého řádku
WAIT 63    MOVE $70 ← 0   MOVE $16 ← 37   ; světlý pruh, X-scroll řádku
WAIT 64    MOVE $16 ← 41
WAIT 71    MOVE $70 ← 8                   ; tmavý pruh
…
WAIT 113   MOVE $17 ← 191                 ; lichý řádek = kopie sudého
…
WAIT 167   MOVE $31 ← 0                   ; panel dole bez posunu

Obloha je jen barva pozadí (registr $4A), kterou copper mění po skupinách řádků, takže vznikne přechod bez jediného bodu v paměti. Hory a krajina na obzoru jsou na tilemapě ve dvou pásech, které se při zatáčení posouvají různou rychlostí (paralaxa). Copper přepne vodorovný posun tilemapy uprostřed obrazovky.

Kopce

Výška segmentů vstupuje do projekce, takže stoupání a klesání silnice vznikají samy. K tomu se přidaly dvě věci, bez kterých by kopce působily ploše.

Kamera se naklání se silnicí. Podle sklonu pod kamerou se posouvá horizont, a s ním i panorama na tilemapě (copper mění její svislý posun) a přechod oblohy. Posun se k cílové hodnotě blíží postupně, takže přejezd horizontu je plynulý.

Nad silnicí se nic nemaže. Když se za kopcem otevře obloha, řádky nad horizontem by bylo potřeba vymazat do průhledné barvy, a to je zase 256 bajtů na řádek. Místo toho copper na začátku snímku nastaví ořezové okno Layer 2 tak, aby vrstva začínala až řádkem clipu. Co je nad ním, se nezobrazí, ať je v paměti cokoli.

Dohled nekončí useknutým pruhem. Poslední promítaná hranice je úběžník s nulovou šířkou na horizontu. Nejvzdálenější úsek do něj plynule přechází v oparu. Blízký vrchol dál správně zakrývá cestu za sebou, ale konec seznamu segmentů už nevytváří další umělý schod.

Úspory, které rozhodly

Na plochém terénu se šířka silnice na daném řádku téměř nemění, takže stačí jen posouvat okna. Kopce mění šířku mnoha řádků. Při opakovaném použití už nakreslených dat je ale potřeba zachovat shodu obou bufferů:

  • Dvojité řádky v blízké zóně. Od řádku 112 dolů se kreslí jen sudé řádky. Na lichých řádcích copper nastaví svislý posun Layer 2 na −1 (registr $17 = 191), takže se zobrazí znovu řádek nad nimi. Blízká silnice je na obrazovce tak velká, že to oko nepozná.
  • Stejná cílová šířka. Oba buffery dostanou stejný řádek. Dřívější tolerance podle naposledy nakreslené šířky ponechávala v každém jinou silnici a při přepínání obraz třásla. Změny šířky se proto neodkládají.
  • Správná sada pruhů. Cache si kromě šířky a posunu pamatuje také čtyřpruhovou nebo třípruhovou sadu. Při změně sady se řádek zkopíruje celý. Blízká promítaná šířka se před interpolací neomezuje na 511 bodů: takové omezení měnilo tvar i rovné silnice.

Každý takový krok se vyplatí změřit na stejné trati ve stejném místě. Některé nápady, které vypadají slibně, měření neobstojí. Například kopírování jen změněné části řádku: DMA je tak rychlé, že čas nežere kopírování, ale řídicí logika kolem něj.

A co objekty?

Stromy, cedule a auta se hodí na hardwarové sprity. Každý objekt stačí předkreslit v pár desítkách velikostí a velké velikosti nechat na hardwarovém zvětšení 2×, 4× a 8×. Za horizontem kopce je potřeba objekt uříznout přesně na hraně silnice. Sprity se ale ořezávat nedají, umí se jen celé schovat. Řada buněk, kterou hrana kopce protne, proto může dostat vlastní kopii grafiky, ve které jsou řádky pod hranou průhledné. To je ale látka na jiný článek.

Pohyb dekorací se počítá z jejich skutečné vzdálenosti 1/z. Při interpolaci mezi promítnutými hranicemi se používá poměr převrácených vzdáleností, nikoli poměr světových vzdáleností. Jinak objekt při přechodu mezi segmenty mění rychlost a poskakuje. Boční odstup se odvozuje od celé kresby, včetně zrcadlení a zvětšení, s rezervou za nejširší krajnicí. Soupeři před zúžením přejíždějí do dostupných pruhů.

Vyzkoušejte si samotnou trať. Soubor tutorial.nex obsahuje jen silnici: zatáčky, kopce, naklánění kamery, oblohu a panorama, bez soupeřů a objektů. A nebo si ho jednoduše spusťte v našem online emulátoru přímo zde:


O doleva P doprava Q plyn A brzda
Musím se přiznat, už teď si hraju s myšlenkou, že nad tímto postavím závodní hru - závodní hru, kde se budou objekty přibližovat plynule, animace auta bude mít spoustu fází, bude k dispozici několik tratí, budete si moci kupovat vylepšení auta... a hlavně Vám pujde o BEST TIME... ;) (ano, tak se jmenovala složka, kde jsem našel tento surový diamant (z mého pohledu), který si vysloveně říká o vybroušení.