int21h

Grafické formáty a práce s jejich daty pod TP7

Další díl seriálu o grafice. Tentokrát už poslední. Samozřejmě, jen co se týče Turbo Pascalu 7. Příště tedy budeme pokračovat ve Free Pascalu, kde se naučíme LFB, práci s Graph, WinGraph, a dalšími jednotkami. Někdy možná i Windows API. A pak už nás čekají jen funkce zvukových karet, sériové a síťové komunikace, pozitiva a sociální jistoty. Dnešní díl bude trochu "chudý", neboť se v něm naučíme hlavně jen pracovat s grafickými soubory (i našimi vlastními), ať už samostatnými, nebo v hromadných souborech. Takže to bude spíše více textu, a méně příkladů a tabulek. No, dost keců a jdeme přímo na věc.


Minule jsme si slíbili, že se naučíme načítat textury z disku. Naučíme se tedy formát BMP. Navíc si ho zjednoduššíme, nebudeme tedy používat kompresi RLE. Existuje sice další spousta lepších formátů, např. PCX (využívá RLE kompresi) nebo GIF (ten ale není volně použitelný), či TGA, PNG, TIF, JPG (ten používá ztrátovou kompresi), atd. Jejich dokumentace se dá dohledat na Internetu, když znáte princip. Ideální je samozřejmě používat vlastní formát, u kterého nehrozí, že Vás bude někdo otravovat, že ho nesmíte používat. Ale k tomu se dostaneme.

BMP je velice jednoduchý formát. V základu jsou jeho data nekomprimovaná a tedy jeho velikost je v zásadě přibližně X*Y*BPP bytů (+hlavička). V souboru najdete vždy hlavičku, pak případnou paletu a pak obrazová data. Ta jsou ukládaná zleva doprava na řádek (jako na obrazovce), ale celkem nelogicky odspodu nahoru (tedy první řádek, který ze souboru načtete, musíte umístit na Yobr+VelY-1). Data jsou uložena stejně jako na obrazovce, tedy pro 8 bitů (BPP=1) připadá 1 byte na pixel, pro 16 dva, pro 24 tři a pro 32 čtyři. Existují samozřejmě i varianty, kdy máte jen 4 bity na pixel (16 barev), 2 (4 barvy) nebo 1 (2 barvy). Paleta je opět uložena stejně jako u 256 barevného (8bit), jen obsahuje namísto 256 pouze 16, 4, či 2 položky (nebo někdy i žádnou). A samozřejmě index zabírá méně, tedy např. u 2 bitového obrázku se do 1 bytu vejdou 4 pixely (8 div 2=4). Ale kdo by takovéto obrázky dneska používal (málo barev, delší dekódování).

Řádky jsou vytvořeny tak, aby jejich délka v BYTECH byla dělitelná čtyřmi beze zbytku. Pokud tedy máte dejme tomu obrázek o šířce 100 pixelů a BPP=3, je vše v pořádku (100*3/4=75). Pokud ale bude velikost už 99, tak je to špatně (99*3=297/4=74.25). Zde se to řeší tak, že pokud je velikost X*BPP AND 3 <> 0, pak na konci každého řádku existuje ještě 3-(X*BPP AND 3) bytů (většinou 0) jako zarovnání (pokud je velikost dělitelná, bude zarovnání 0 bytů, ale na to nesmíme použít ten vzoreček, neboť by nám vyšlo 3!). Tyto byty se přečíst/uložit musí, ale nebudou se zobrazovat (až budeme později používat kompresi v našem formátu, je vhodné tyto byty nastavit podle bytu posledního pixelu na řádku, pokud budeme ovšem zarovnávat).

Řekli jsme si, že na začátku každého souboru je hlavička. Protože existují dva formáty, nemůžeme jí načíst celou najednou. Nejprve pomocí BLOCKREAD načteme ze souboru první část, která je společná:


var	BMPhlav : record
		   ID : array[0..1] of char;
		   VelSoub : longint;
		   Rezeva : longint;
		   StartObrDat : longint;
		   DelkaHlav : longint;
		  end;  

ID bude vždy obsahovat znaky 'BM', jinak to není formát BMP (a to i tehdy, pokud má příponu .BMP; naopak nám samozřejmě nic nebrání přejmenovat soubor na jinou příponu). VELSOUB obsahuje velikost celého souboru v bytech (máte tak kontrolu, zda se opravdu jedná u správně vyplněnou hlavičku). REZERVA se nepoužívá a většinou zde bývá 0. STARTOBRDAT ukazuje na první obrazová data (offset v bytech v souboru). Nás zajímá DELKAHLAV, která udává, zda se jedná o verzi OS/2 (12 bytů) nebo Microsoft (40 bytů). Podle verze dočteme zbytek hlavičky:


var	OS2 : record
	       VelX, VelY : word;
	       Palet : word;
	       BituNaPix : word;
	      end;


	MS : record
	      VelX, VelY : longint;
	      Palet : word;
	      BituNaPix : word;
	      Komprese : longint;
	      VelDat : longint;
	      MaxX, MaxY : longint;
	      Barev : longint;
	      DuleziteBarvy : longint;
	     end;  

Verze OS/2 je jednodušší. Zde najdete jen POCETPALET, který bývá většinou 1 (hlavně pro BPP=1). Velikost je udávaná v pixelech. BITUNAPIX je 8,16,24, atd. Verze Microsoft si některé údaje zvětšuje, avšak nadále platí, že max.velikost XY v BMP je někde okolo 65536 pixelů pro obě osy (4 GB velký soubor pro BPP=1). Komprese je 0 pro žádná a 1 pro RLE (2 pro 4 bitovou RLE). VELDAT udává velikost obrazových dat v bytech. MAX* znamenají, kolik pixelů může být takový obrázek maximálně (většinou si to nastavuje editor, který soubor vytvořil a pohybuje se cca. okolo 3 tisíc pixelů podle aktuální velikosti paměti RAM). BAREV je nastaveno na 256, pokud je BPP=1. DULEZITEBARVY bývají většinou na 0, kromě BPP=1, viz. dále. Jakmile načteme hlavičku a BPP není 2-4, musíme ještě načíst paletu.

Paleta je uložena jako DULEZITYCHBAREV*4 bytů hned za hlavičkou (stačí nám pole ARRAY). Tedy pokud máte barev 256, zabírá paleta 1024 bytů. Data jdou v pořadí 0BGR, protože MS byl posedlý zarovnáním na 32 bitů. To platí pro verzi Microsoft. Ve verzi OS/2 existují jen 3 byty na složku a navíc jdou v pořadí RGB, tedy opačně. Paleta pak má jen 768 bytů. Musíme umět číst obojí. Microsoft navíc dost často dělá zradu. I pokud je obrázek 8 bitový, nemusí být v paletě uloženo všech 256 barev. To je užitečné v případě, kdy je např. v celém obrázku jen 20 odstínů jedné barvy a víc nic. V paletě tedy bude uloženo jen 20*4 bytů. Dobře, to je vhodné pro ušetření místa. Občas ale v DB najdete 0! To znamená, že počet barev je 256 (potvora, co?). Všechny složky jsou ve formátu 0-255, tedy než je případně pošlete do DAC (pokud byste chtěli zobrazit jen 1 BMP obrázek současně), musíte je opět vydělit 4x (pokud je ale budete chtít zpracovávat např. na plochu s BPP<>1, ponechte je tak, jak jsou). Komprese RLE funguje u BMP pouze pro BPP=1 a jen ve verzi Microsoft (my se jí naučíme i pro ostatní BPP). Paleta ale RLE nepoužívá nikdy a je uložena vždy (pro BPP<2) hned za hlavičkou (u některých jiných formátů je uložena až za daty, někdy za hlavičkou nebo daty v závislosti na počtů bitů na pixel (4/8)).

A nyní už nám stačí jen načíst data (pixely) podle informací v hlavičce. Tímto jsme přečetli naše textury a nyní s nimi můžeme pracovat podle příkladů, které jsme se naučili minule. Ten kdo umí číst, umí i psát, takže můžeme i típat naši obrazovku a ukládat na disk přes BLOCKWRITE. Jen se zmíníme o optimalizaci práce s diskem. Čím méně přístupů na něj máte, tím lépe, a číst/psát data po 1 pixelu nebo dokonce bytu je sebevražda (čtení po 128 bytech zrychlí práci s HDD (i dnes) až 100x, čtení po 100 kB až 1000x, někdy i (řádově) vícekrát (!!!), dle HW diskové cache). Ideální je načíst celý obrázek do paměti najednou (pokud je větší než 64 kB, tak pomocí vícero volání BLOCKREAD do XMS). Pokud se tam ale nevejde, nebo ho nepotřebujeme celý (či ho potřebujeme jen 1x), je vhodné si rozpracovat něco na způsob čtení dopředu. Uděláme si funkce, které budou jakoby číst z disku po 1-X bytech. Tyto funkce ale ve skutečnosti zavolají naši další funkci, která zjistí: a) kam máme skočit (náhrada SEEK), b) kolik toho chceme přečíst. Pokud data pro SEEK ještě nejsou v paměti, nahraje do RAM např. 32 kB dat od této pozice. Pak, když je budeme chtít číst, vrátí nám data z tohoto bufferu (a ne z disku), tedy je můžeme požadovat třeba po bytech a přesto to bude rychlé. Pokud budeme už na konci bufferu, funkce jednoduše dočte další blok a vrátí nám data ze začátku bufferu.

Dobrá, data máme, umíme je barvit či měnit jas. Umíme je vlastně i invertovat. Stačí, když každý pixel změníme přes NOT. Tím vlastně provedeme inverzi obrázku (černý na bílý, atd.). Protože NOT v assembleru dokáže zpracovat až 4 byty najednou, je vhodné nepracovat s jednotlivými pixely, ale s bloky 4 bytů. Pokud má obrázek méně než 4 BPP, urychlíme tím jeho zpracování (NOT na 4 pixelech s BPP=1 je až 4x rychlejší než 4x NOT na 1 bytu). Hodí se to i pro BPP=3, kdy třikrát NOT zpracuje 4 pixely (navíc rychleji, než 12x NOT po složkách těchto pixelů). Fajn, co nějaké jiné operace? Můžeme obrázek zrcadlit. Pokud ho chceme zrcadlit podle osy Y, stačí vždy, když prohodíme pixel I a X-I-1, kde I bude v intervalu <0,X shr 1> (X je šířka obrázku v pixelech, ale obrázek je uložen v poli [0..Y-1] of BPP). Toto provedeme pro každý řádek. Zrcadlení podle osy X je rychlejší. Provádíme to opět pro 0..Y-I-1, ale ne po 1 pixelu, ale pomocí MOVE po celém řádku (vždy si jej musíme někam uchovat stranou, tj. budeme potřebovat buffer o velikosti X*BPP bytů). Prohazování údajů jsme se naučili minule při třídení tabulek s barvami. Zrcadlení podle "os os" X nebo Y (tedy podle úhlopříček) je trochu složitější. Pokud zrcadlíme podle osy 0,0 až X-1,Y-1, tedy zleva nahoře doprava dolů, tak čteme pixely od 0 až do X-1 na řádku 0 a ukládáme je do sloupce 0 od Y-1 do 0. Druhý řádek budeme číst od 1 (pokud je obrázek čtvercový a tedy rozdělen na rovnostranné trojúhelníky) do X-1 na řádku 1 a ukládat do sloupce 1 od Y-1 do 1. A tak až do počtu řádků = Y, kdy na posledním řádku Y-1 vlastně přečteme ideálně max. 1 pixel a prohodíme ho buď s jedním (sudý počet), nebo s ním samým (lichý počet). Tyto operace už ale nejsou tak snadné.

Obdobné to je s rotací. Nejjednodušší je rotace po 90 stupních. V každém případě je pro zjednodušení dobré použít druhý buffer, do kterého budeme ukládat výsledek. Mohli bychom samozřejmě použít také výměnu (máme 4 pixely, které potřebujeme otočit a přepsat navzájem aniž bychom ztratili byť jen jediný z nich) B->AA, A->B, C->BB, AA->C, D->A, BB->D, ale je to více operací, než když prostě přečteme pixel ze zdrojového buferu, a umístíme na jiné místo do bufferu dalšího, kdy se to zredukuje na A->B', B->C', C->D' a D->A', ale pravda, za cenu 2x tak velké spotřeby paměti (zatímco u první varianty by nám stačilo jen 2*BPP bytů (max. 8)). Celá činnost vlastně spočívá v tom, že podle toho, zda chcete obrázek naklánět doprava nebo doleva umísťujete byty na nové pozice. Nejlepší je rotace o 180 stupňů, kdy se vlastně XY velikost obrázku nezmění, Vy jen provedete zrcadlení podle X a následně podle Y (nebo obráceně). Rotace o 90 stupňů už vyžaduje (kromě prohození rozměrů X<>Y), abyste četli pixely z řádku 0 v rozmezí 0 až X-1 a umisťovali je do sloupce Y v druhém bufferu na pozice v rozmezí 0 až Y-1 (pokud rotujete doprava) nebo na sloupec 0 a pozice Y-1 až 0 (pokud rotujete doleva). Zde vždy pracujete se stejným počtem pixelů, zatímco v první metodě bez druhého bufferu byste vždy použili o 1 pixel méně v každém novém průchodu, takže by metoda s pracovními pixely AA a BB mohla být i úspornější. Záleží jen na Vás, kterou z nich si vyberete.

Rotace o jiný úhel než 90/180 stupňů je trochu náročnější na výpočet a každopádně potřebuje další buffer (tady se to už nedá "ukecat"). Budeme využívat pohyb bodu po kružnici a tedy něco z goniometrie. Tam se vyskytuje SIN a COS, a jak víme, tyto výpočty se neobejdou bez reálných čísel a koprocesoru. Navíc výpočet Sinu či Cosinu je dost náročná operace (ano, i v dnešní době je stále pomalejší než prosté + či -). Použijeme tedy dva triky. Za prvé nahradíme reálná čísla celočíselnými tak, že použijeme 32 bitová čísla. Tedy -1 bude 0, 0 se stane $80ffffff a 1 se stane $ffffffff. To by šlo ve Free Pascalu, který zná číslený typ DWORD bez znaménka. Protože ale v TP7 je pouze LONGINT, který znaménko má, musíme snížit rozsah buď na WORD (méně přesné a pomalejší na 32 bitových CPU), nebo jednoduše posunout 0 o jeden řád dolů (stejně tak +1, to se stane jaksi automaticky). Jednodušší bude ale, protože potřebujeme i záporná čísla, vyjádřit -1 až +1 jako -65536 až +65536 v typu Longint (jak TP7, tak FP7). Za druhé, použijeme již předpočítané tabulky. Ty si můžeme buď vypočítat doma na kalkulačce, kde můžeme případně čísla upravit, aby byla jemněji stupňovaná, nebo nechat počítač, pokud má NPU (dneska všechny), aby si tabulku vypočítal při startu programu (hlavně to nesmíte míchat s MMX (ty musíte psát v TP7 jedině přes opkódy, FP je ale už zná: pokud máte SSE, tak namísto NPU použijete SSE a to míchat s MMX můžete - jen pozor na SSE2, které používá i registry MMX)).


var	Sinus[0..359] of longint;
	Cosinus[0..359] of longint;  

Tabulky můžeme i zkrátit, neboť víme, že obě vlny se vlastně opakují. Tedy Sinus 0 = Sinus 180, Sinus 1 = Sinus 179. Stejně tak Sinus 90 = - Sinus 270, atd. Stejně tak víme, že hodnoty pro SIN a COS jsou stejné, jen posunuté o 90 stupňů (takže by stačila i jen jedna zkrácená tabulka). Takže můžeme tabulky zkrátit až na 1/4 či 1/8, ale pak budeme muset podle čísla úhlu upravovat výsledek a to nás může "bolet" více než ten necelý kB (na 1 tabulku), který můžeme ušetřit (zvláště v FP na toto můžeme bezstarostně "plivat"). Použijeme tedy kompletní tabulky a náš výpočet pak bude vypadat takto (nezapomínejme na to, že funkce v TP7 vyžadují úhly v radiánech a ne v úhelech, takže je ještě musíme převést pomocí vzorce UHEL*PI/180 (nebo dělíte úhel číslem 57.2957795; převod zpět je RAD*180/PI; pomocí kalkulačky a znalosti, že tam bude PI a 180, na to musí přijít každý)):


var	i : longint;
begin
 for i := 0 to 359 do
 begin
  Sinus[i] := Round(Sin(i/57.2957795)*65536);
  Cosinus[i] := Round(Cos(i/57.2957795)*65536);
 end;
end;  

Teď, když budeme chtít vypočíst nějaký úhel, nemusíme volat SIN a COS, ale stačí nám provést jen toto:


begin
 Hodnota := Sinus[uhel] div 65536;
 Hodnota := Cosinus[uhel] div 65536;
end;  

Samozřejmě, že dostaneme vždy jen hodnotu -1, 0 nebo 1. Nic mezi tím. Ale nám nešlo o znalost přesné hodnoty úhlu. My potřebujeme toto pro výpočet, kdy dělit budeme teprve až poté, co vynásobíme dané číslo nějakou jinou hodnotou. Zde tedy máme hodnoty -65536 až 65536. Pro -1 až 1. Fajn. K čemu nám to je?

Pokud chceme provádět rotaci, tak musíme znát vzoreček. Nebudu Vás napínat, ten zní takto pro X a Y každého bodu:


begin
 x2 := (x1*cosinus[uhel]-y1*sinus[uhel]) div 65536;
 y2 := (x1*sinus[uhel]+y1*cosinus[uhel]) div 65536;
end;  

Jedná se o přepočet starých XY (1=v bufferu) na nová XY (2=na obrazovce, ve VVRAM). Dobré efekty můžeme udělat např. tak, že si tabulku uděláme jako WORD, a zbytek necháme, nebo že si ve vzorečku pro Y2 prohodíme Cosinus a Sinus. Stejně tak můžeme měnit znaménka (buď aditivně nebo nezávisle na sobě), kdy se nám bude objekt různě vrtět, deformovat, překlápět, atd. Je jen na Vás, jak vyzkoušíte různé kombinace. Také platí, že pokud číslo za DIV 2x zvětšíte, objekt se Vám rovnou 2x zmenší a tedy zabijeme dvě mouchy jednou ranou (kdybychom to potřebovali). Teď pro každý bod vezmeme pixel ze souřadnic X1,Y1 a umístíme jej do VVRAM na souřadnice X2,Y2.

Asi jste si ale všimli, že se nám textura otáčí, ale jaksi ne podle svého středu, ale podle levého horního bodu textury. Není nic snažšího. Stačí, když si určíme bod středu otáčení. Ten si označíme jako XS a YS (obvykle bývají "X shr 1" resp. "Y shr 1") a upravíme příslušné rovnice:


begin
 x2 := ((x1-xs)*cosinus[uhel]-(y1-xs)*sinus[uhel]) div 65536;
 y2 := ((x1-xs)*sinus[uhel]+(y1-ys)*cosinu[uhel]) div 65536;
end;  

Objekt nám rotuje po směru hodiných ručiček. Pokud chceme obrácený pohyb, stačí úhly zmenšovat (359 až 0). Musíme samozřejmě ještě testovat, zda se vypočtený pixel vejde do hranice VVRAM. Zda má cenu tento test provádět u každého pixelu si můžeme spočítat dopředu, kdy oblast, kterou může objekt zabrat, zvětšíme o 1.414 na obě strany (natočíme-li objekt o 45 st., bude jeho roh v nejhorším případě vrchol rovnostranného trojúhelníku, jehož výška od základy bude odmocnina ze dvou) a pokud se nám tedy souřadnice objektu na obrazovku vejdou, nemusíme pak testovat každý pixel zvlášť (1.414 můžeme vyjádřit také jako 92668 a dělením 65536)). Pokud musíte provádět test u každého pixelu, bude to vypadat nějak takto:


if (x > 0) and (x < MaxX) and (y > 0) and (y < MaxY) then  

Dobrá, ale nešlo by to nějak urychlit? Přeci jen, to dělení... no, tak snadné to nebude. To dělení totiž nemůžeme jen tak vyhodit, jelikož potřebujeme záporné hodnoty a na ty nelze jen tak aplikovat SHR. Budeme potřebovat pracovní proměnné. Do první si uložíme maximální oblast, do které je možné kreslit, ale zvětšinou 65536x. Do další si uložíme souřadnice na obrazovce, na které chceme objekt kreslit (také zvětšené). Nejprve budeme tedy pracovat s velkými čísly a teprve, pokud bude číslo >0 a zároveň ležet v oblasti VVRAM, můžeme na něj pustit SHR a SHL, a vykreslit jej. Pro rozlišení 320x200, obrázek o velikosti 128x100 otáčeného kolem svého středu a kresleného na 160x100 přímo do VRAM na kartě to bude vypadat následovně (výpočet tabulek zůstane stejný):


var	x1,y1,x2,y2,x,y,a,PomX,PomY : longint;
	Uhel : word;
	Obrazek : array[0..99,0..127] of byte;
begin
 X1 := 160 shl 16;
 Y1 := 100 shl 16;
 X2 := 320 shl 16;
 Y2 := 200 shl 16;
 for Uhel := 0 to 359 do
 begin
  for PomY := 0 to 99 do
   for PomX := 0 to 127 do
   begin
    x := ((pomx-64)*cosinus[uhel]-(pomy-50)*sinus[uhel])+x1;
    y := ((pomx-64)*sinus[uhel]+(pomy-50)*cosinus[uhel])+y1;
    if (x > 0) and (x < X2) and (y > 0) and (y < Y2) then
    begin
     a := y shr 16;
     Mem[$a000:a shl 8+a shl 6+x shr 16] := Obrazek[PomY,PomX];
    end;
   end;
 end;
end;  

Nutno podotknout, že na jediném obrázku zapisovaném přímo do VRAM nepoznáte rozdíl mezi tímto a variantou, kde budete mít DIV 65536 a *320 u souřadnic (na Pentium II s 350 MHz CPU). Ovšem u slabších počítačů, s více obrázky, se zápisem do VVRAM, se to už může projevit markantněji.

Další věcí, kterou jistě u obrázků užijete, je změna velikosti. Nutno podotknout, že pokud číslo 65536 za DIV 2x zvětšíte, tak se Vám obrázek 2x zmenší, atd. Ale zvětšovat takto nejde (byly by mezi pixely mezery). Nejjednodušší je změna velikosti v násobku 2x. Pokud chceme obrázek 2x zmenšit, odebereme každý 2. pixel. Pokud 4x, odebereme vždy 3 pixely na 1, který ponecháme. Pokud chceme obrázek zvětšit, tak naopak při 2x každý pixel zkopírujeme. Pokud 4x, tak ho zkopírujeme 3x. Obrázek se nám ale stane jaksi zubatým. Proto je vhodné použít metodu antialiasingu. Ta při zvětšení spočívá v tom, že při 2x zvětšení obrázku s pixely ABCD neuděláme AABBCCDD, ale A1B2C3D4, kde čísla 1234 se spočítají jako průměr složek sousedů, tedy 1=(A+B) shr 1, 2=(B+C) shr 1, atd. (myslím tím samozřejmě složky RGB). Při zmenšování je možné nejprve spočítat u pixelů ABC průměr A a B, poté B smazat a nahradit pixel A pixelem, který vzniknul zprůměrováním. Dobře, to máme ale dost velké zvětšení. Co když chceme zvětšit nebo zmenšit obrázek jen o pár %? Samozřejmě, při zmenšování je možné, když prostě pixely čteme postupně z pozic 0,1,2,3 (prostě "n") a budeme je umisťovat třeba do dalšího bufferu na pozice "(Zmenseni*n) shr 8", kde Zmenseni=<0,255> pro 0-99.9% (pro zvětšování >100% (>255) Vám tam ale vziknou tečky a ne celistvý obrázek, navíc to násobení je trošku pomalejší na starších počítačích). Ale můžeme také použít metodu, která si bere něco z kreslení čar (či-li využívání přírůstků). A to jak pro zmenšení, tak i pro zvětšení (a samozřejmě je to možné kombinovat s metodou vyhlazení popsanou výše).

Nejprve budeme potřebovat znát poměry pro řádky a sloupce (pokud nebude obrázek čtverec, tak budou oba jiné). Ty vypočítáme třeba takto pro zmenšení:


	PomerX := (X shl 32) div ((X shl 16) - (X * Pomer));
	PomerY := (Y shl 32) div ((Y shl 16) - (Y * Pomer));
(*
	PomerX := Round(X div (X - (X * PomerR)) * 65536);
	PomerY := Round(Y div (Y - (Y * PomerR)) * 65536);
*)

nebo pro zvětšení:


	PomerX := ((X * Pomer) div X);
	PomerY := ((Y * Pomer) div Y);  

Budeme brát, že 65536 je 100%. Pokud tedy chceme 2x zmenšovat, zadáme 32768. Pokud 2x zvětšovat, zadáme 131072. POMER může být pro X a Y osu jiný (můžeme klidně počet řádků zmenšovat, ale počet sloupců zvětšovat - tedy budeme zvětšovat jen ty řádky na X, které nám zbydou po zmenšení Y). To, co nám vyjde je číslo, které budeme přičítat jako rozdíl (při zvětšování) nebo se ho budeme snažit dosáhnout (při zmenšování). A teď. Jak na to. Pro zmenšování nám vyšlo určitě číslo >65536 (což bereme jako 1). POMER musí být 0-65535 (POMERR by byl 0.01 až 0.99 pro 1% až 99%). Pro zvětšování nám zase vyšlo číslo menší než 65536. Teď, podle toho, co chceme dělat, budeme provádět:


Společné:

1) nastavíme si A na 0
2) do B si dáme náš poměr <65536 či >65536
3) do C si dáme 0, jakožto číslo zdrojového akt.sloupce/řádku
4) do D si dáme totéž (0), to bude ukazovat cílový sloupec/řádek
5) opakujeme:

Pro zvětšování:

a) uložíme aktuální pixel/řádek do cíle (D+1)
b) provedeme A := A+B
c) pokud A >= 65536 pak:
  - A := A - 65536;
  - uložíme současný pixel/řádek (C) ještě jednou (a zase D+1)
d) provedeme C+1

Při zmenšování:

a) provedeme A := A+65536
b) pokud A < B pak
  - uložíme současný pixel/řádek (D+1)
pokud A >= B pak
  - provedeme A := A - 65536;
c) provedeme C+1

A to je všechno, co se asi tak dá v základu provádět s texturami ve 2D.

Už tedy umíme načíst do paměti nějakou tu texturu a pak s ní umíme pracovat. Pokud Vám ale nevyhovuje formát BMP, můžete si vymyslet vlastní, napsat si nějaké programy, které např. převedou BMP na Váš formát (a zpět), popř. už přímo grafický editor s výstupem do Vašeho formátu. A abychom neskončili jen u obrázků, uděláme si vlastní formát i pro animace (video je to samé, jen přidáte zvukovou stopu, ale to budeme brát někdy příště).

Náš formát bude podobný BMP. Budeme zapisovat pixely odshora dolů po řádcích (zleva doprava po pixelech), zarovnání na 32 bitů nechám na Vás (ale rozhodně bych ho doporučoval, i když zarovnat obrázek můžete až později v programu prostě tak, že ho budete číst po řádcích, které umístíte na adresy dělitelné čtyřmi). Příponu si můžete vymyslet jakou chcete. Co budeme ukládat do hlavičky?


var	NasFormat : record
			X,Y : longint;
			BPP : longint;
			Komprese : longint;
			Pruhl : record
				 R,G,B : byte;
				end;
			Maska : byte;
		    end;  

Co se týče rozměrů, tak ty jsou asi jasné. BPP nám udává, kolik bytů na pixel mají barvy. Pokud chcete, můžete si sem ukládat počet bitů (8,16,24,32), když chcete podporovat i 4 bitové, ale já sem ukládám byty (tj. 1,2,3,4). Bitová informace má tu výhodu, že můžete rozlišit i 15 a 16 bitů, pokud budete někdy něco takového potřebovat (Váš formát nemusí umět uložit 15 bitové pixely, ale (Váš) editor by měl být schopný si to převést). Údaj o kompresi nám bude značit, zda náš formát využívá RLE (1) nebo ne (0). Klidně by mohl mít velikost BYTE, ale je to jen kvůli zarovnání (také bychom ho mohli dát na konec hlavičky, kde by jeho bytová velikost nevadila). PRUHL nám udává, která barva bude průhledná (tedy se nebude kreslit vůbec; jedná se o 1 bitový alfa kanál). U BPP<>0 se využijí všechny tři složky pro popis (u BPP=2 budou už v mezích 0-31, či 0-63), u BPP=1 se použije třeba jen R pro index barvy (většinou 0). MASKA nám udává, jak je v obrázku uložena maska průsvitnosti (0=znamená, že pro BPP<>4 není maska uložena vůbec a pro BPP=4 je uložena jako 4. byte každého pixelu; 1=že je uložena jako zvláštní 8bitový obrázek hned za tím normálním; 2=maska se nepoužívá/není uložena).

Program, když nahraje Váš obrázek do paměti, tak nejprve zjistí, jaké to je BPP. Pokud se jedná o 1, měl by načíst ještě paletu (tu můžete uložit stejně jako BMP hned za hlavičku, o tom, kolik bytů na složku budete mít si rozhodněte sami). Pokud tedy bude BPP=1, tak pro každý pixel z pole neuloží do VVRAM (BPP<>1) jeho index, ale složky RGB, které podle jeho indexu najde v paletě obrázku. Pokud bude BPP jiné, tak může načítat rovnou pixely. Pokud bude pixel roven průhledné barvě, tak se nebude vůbec kreslit a hned se přejde na další (můžete si také zavést další byte, který pro hodnotu 0 bude znamenat, že obrázek průhledný není a je možné celý řádek přesunout pomocí MOVE do VVRAM, což jeho zobrazení velice urychlí). Pokud tedy pixel nebude průhledný, tak se ještě otestuje, zda se používá maska průsvitnosti. Pokud ne, tak se prostě pixel vykreslí. Pokud ano, tak je buď uložena jako 4. byte právě načteného pixelu, nebo (vhodné pro BPP<4, aby také mohly mít masku) se načte z obrázku, který je uložen jako další obrázek hned za tím stávajícím, ale má stejnou velikost. Podle toho, jaká je hodnota pixelu (0-255) se vypočte mix. pixelu z RGB a toho, který je ve VVRAM.

Formát můžeme obohatit tím, že umožníme uložit více vrstev (nebo více různých verzí obrázků). Ty budou mít stejnou XY velikost i BPP, takže vlastně jen přibyde další údaj do hlavičky - VRSTEV. Pokud bude MASKA=1, bude obrázek+maska vždy pohromadě pro každou vrstvu. Ovšem je i možné, aby každý takový obrázek měl vlastní XY a vlastní BPP. Pak jen stačí, před každá obrazová data dát další pod-hlavičku, která bude mít jen X,Y a BPP údaje a podle nich se daná data načtou. Náš formát tedy bude obsahovat tyto různé bloky dat (a může být i variabilní):

	Hlavička		SizeOf(Hlavička)
	[Paleta]		768/1024 bytů a méně
	Obrazová data		X*Y*BPP bytů
	[maska]			X*Y bytů
	[Obrazová data		X*Y*BPP bytů
	[maska]			X*Y bytů]
	[...
	[Obrazová data		X*Y*BPP bytů
	[maska]			X*Y bytů]]  
Varianta s různými XYB pro každou vrstvu bude vypadat podobně:
	Hlavička		SizeOf(Hlavička)
	[Paleta]		768/1024 bytů a méně
	Obrazová data		X*Y*BPP bytů
	[maska]			X*Y bytů
	[hlavička1		SizeOF(hlavičkaN)
	 Obrazová data		X1*Y1*BPP1 bytů
	[maska]			X1*Y1 bytů]
	[...
	[hlavičkaN		SizeOF(hlavičkaN)
	 Obrazová data		Xn*Yn*BPPn bytů
	[maska]			Xn*Yn bytů]]  

Každá vrstva může navíc používat (buď všechny nebo zvlášť) ještě navíc kompresi RLE. Existují i ostatní typy (např. ztrátová, jakou používá JPEG), ale samotná RLE má tu výhodu, že nemění vzhled obrázku (pravda, je poněkud méně účinná). My si zavedeme takový typ, že vždy nejvyšší bit v prvním pixelu bloku udává, zda zbytek bitů (7,15,23,31) značí počet pixelů pro opakování (1) nebo počet pixelů, které jsou jedinečné (0). Tedy v BPP=1 můžeme zkomprimovat do 2 bytů až 127 (128, pokud budeme brát že 0=1, 1=2, atd.) pixelů, nebo si označit, že až 128 pixelů po 1. bytu se prostě jen přečte. Pro BPP=2 to je už 32768, pro BPP=3 (a 4. pro MASKA=0) něco přes 8 miliónů. Obrázek můžeme komprimovat a dekomprimovat za pomoci pracovního bufferu. Komprimovaná a nekomprimovaná data mohou vypadat nějak takto:


	85h ffh 03h 9h 8h 7h -> $ff $ff $ff $ff $ff $9 $8 $7  

RLE komprese může dosahovat průměrně 0-99% účinnosti v závislosti na tom, jaká zde máte data (pro VELMI složité obrázky může být dokonce komprimovaný soubor větší než originál, takže by Váš editor měl rozhodnout, že pokud bude obrázek menší než 95% originálu, kompresi ponechá, jinak ji vypne a obrázek uloží bez ní (5% úspora je moc malá vzhledem k časové ztrátě nutné k dekomprimování takového obrazu). Můžete komprimovat každý řádek zvlášť (což se vyplatí v případě, že bude např. obrázek uchovávat i v RAM komprimovaný a budete chtít vykreslit jen některý řádek, např. zrovna ten poslední, tak stačí začít dekomprimovat jen jeho - pokud si nejprve označíte, kde začíná) nebo přes řádky (což může výsledný soubor o trochu zmenšit), jako kdyby to byl souvislý tok dat (namísto Y řádků o X pixelech to budete brát jako X*Y pixelů na 1 řádku). Dekompresor je jednoduchý. Prostě přečtete BPP bytů a zjistíte, zda je vrchní bit 1. Pokud ano, zopakujete dalších BPP bytů tolikrát, kolik je v dolních bitech předchozí skupiny BPP bytů zadáno. Pokud je bit 0, tak přečtete tolik BPP bytů, kolik je zadáno v dolní části aktuální skupiny BPP. Kompresor vlastně jen jede po pixelech a pamatuje si ten předchozí. Pokud je aktuální stejný jako předchozí, zahájí kompresi (počítá počet stejných pixelů). Jakmile narazí na různý, uloží počet pixelů a hodnotu jednoho z nich, a začne počítat počet různých pixelů, než zase narazí na dva stejné.

Jistě jste si všimli, že spousta her používá jeden velký (PAK, GRP, WAD) soubor a nemá všechny obrázky, atd. poházené v adresáři jako velkou spoustu souborů. Vytvořit si takový soubor je vlastně velmi jednoduché, když už umíme dělat více vrstvé obrázky. Máme dvě varianty. Buď náš soubor bude obsahovat na začátku tabulku, která bude obsahovat jména souborů, jejich typ (pokud tam budete mít i jiná data než obrázky), jejich pozici (offset) v souboru, a případně i velikost původních souborů (na HDD). Tento způsob má výhodu v tom, že najít požadovaný obrázek je rychlejší, vzhledem k tomu, že vlastně procházíte jen malý blok dat. Nevýhoda je, když chcete nějaký soubor vymazat, tak musíte změnit offsety všech souborů, co leží za ním. Druhá varianta je na toto výhodnější. Soubory jsou uloženy v hromadném souboru hned za sebou s tím, že před každý souborem je pouze údaj o jeho jméně (8 znaků) a 4 bytový údaj o jeho velikosti. Tedy, pokud chceme nějaký z nich vymazat, stačí jen zbytek dat, co leží za ním posunout na jeho místo o jeho velikost (a nemusíme nic dalšího měnit). Naopak, pokud budeme nějaká data chtít najít, musíme postupovat tím stylem, že přečteme 12 bytů a porovnáme, zda jméno souhlasí. Pokud ano, zpracujeme soubor, pokud ne, musíme skočit o LONGINT bytů dále a opakovat hledání. Tato varianta se mi zdá lepší, protože je možné daný soubor nahrát do paměti celý, a vytvořit si rovnou extra tabulku s indexy daných dat (je ji možné samozřejmě použít i při hledání těchto dat přímo z disku).

Pokud umíme kreslit obrázek, umíme i animaci. Ta vlastně není nic jiného než obrázky kreslené rychle po sobě. Můžeme buď použít samostatné obrázky nebo animaci uloženou jako vrstvy v jednom obrázku (jejich počet je prakticky neomezen, protože pokud za vrstvou už není konec souboru, je tam další vrstva, atd.). Nevýhoda je ale velikost. Ono uložit třeba 32 snímků animace (její trvání je závislé na její rychlosti. Pokud Váš engine jede např. na rychlosti 25 Hz, tak pokud jeden snímek animace potrvá 1 fps, bude 25 snímků trvat 1 vteřinu. Pokud dáte každému snímku 2 fps, za jednu vteřinu jich bude přibližně 12, tedy animace s počtem snímků 25 potrvá 2 vteřiny) o rozměrech 32x32 bytů s BPP=2 bez masky 64 kB (a pokud jich v úrovni/hře/levelu budete chtít mít několik (desítek), tak potěš pánbůh...). Pro vykreslení animace (pokud má být např. průhledná do pozadí a ne do předchozího snímku (pokud průhledná ani průsvitná není, nemusíte ji obnovovat, ale pokud má po skončení zmizet, musíte mít uschované pozadí, co bylo pod ní (podobně se kreslí kurzor myši: uschováte, vykreslíte. Pokud se pohnul, obnovíte a zase uschováte a vykreslíte kurzor)) můžete tedy přidat další informace do její hlavičky, jako délka jednotlivých snímků v počtu FPS, atd.

Lepší a úspornější metoda (jak na HDD/DVD/CD, tak i na RAM) je ale uložit animaci komprimovanou jinou metodou. Protože se dá předpokládat, že se v animaci mění pouze určitá data (např. pokud panáček mává, jistě se mu nemění bota), využijeme rozdílovou kompresi. Ta nekomprimuje pixely v závislosti na svých levých sousedech, ale na pixelech předchozího snímku. Je jasné, že první snímek zůstane bez komprese, nebo jen s RLE. Pro ostatní si vytvoříme tzv. masku. Ta bude uložená před každým snímkem (resp. "vrstvou") za jeho hlavičkou. Její velikost bude X*Y DIV 8 (dorovnaná na celé byty nahoru). Každý bit zde reprezentuje 1 pixel (1 bitový alfa kanál), tedy maska bude 8x menší než samotné pole pixelů. Pokud bude bit 1, je hned za maskou uložen pixel, který se má na danou pozici uložit. Pokud je tam 0, jdeme na další pozici X, případně Y. Pozice v pixelech uložených za maskou se zvyšuje jen při každé 1, zatímco pozice na obrazovce po každém bitu masky. Čtečka tedy čte masku, mění pozice a při "1" přečte z pole pixelů hodnotu a umístí na danou pozici. Zapisovačka musí mít v paměti oba snímky současně a vždy otestuje, zda pixel na novém snímku je odlišný od pixelu předchozího. Pokud ano, umístí do masky "1" a pixel zapíše do fronty dat. Je jasné, že např. u temného vesmíru, kde jsou jen tečky jako hvězdy, bude mít tato komprese velký úspěch. Tuto kompresi lze rozšířit tak, že se nemusí ukládat celá maska, ale jen ta oblast, která se změnila (ideálně jen po řádcích, tj. pokud se změnil nápis uprostřed obrazovky, uloží se ten ta část masky, která zasahuje do tohoto nápisu: změny mezí Y1 a Y2 probíhají stejně jako zápis změněných banků při zobrazování VVRAM). Druhým rozšířením je tolerance. Pixel se bere jako shodný, pokud jeho složka +/- jistá tolerace je >=< se složkou (pro každou z R,G,B) předchozího pixelu. Tím se dá drasticky vyhodit počet dat nutných k uložení, ale je jasné, že tato metoda je ztrátová. Proto je vhodné po určitém počtu snímků (25=1 vteřina? i 10 vteřin, pokud je tolerance malá (tj. do 4)) vložit tzv. snímek klíčový, který bude komprimován jen pomocí RLE stejně jako vůbec první snímek v animaci. Obrazová data nebo i maska mohou být komprimovány také pomocí RLE (maska totiž může obsahovat dost bytů plných 1 nebo 0; a obrazová data budou např. při výbuchu také plná stejných nových pixelů). Je tedy nutné přidat do hlavičky údaje, které o tom budou informovat dekódovací program.


Tak, věřte nebo ne, ale jsme na konci. Co se týče grafiky, tak se už k Turbo Pascalu 7 nebudeme vracet. Příště se podíváme na grafiku ve Free Pascalu (samozřejmě, vše, co bylo doposud uvedeno, platí i pro FP) a tím uzavřeme kapitolu o grafice úplně. Pak už nás čekají jen zvuky a mezipočítačová komunikace.

2006-11-30 | Martin Lux
Reklamy: