int21h

Plynulá animace pomocí unitu Graph

Přestože grafikou jsme se tu zabývali už mnohokrát, dosud jsme se tu nedotkli jednotky Graph
Ve Freepascalu funguje jednotka Graph podobně jako obvyklé grafické knihovny, ale v Turbo pascalu funguje poněkud jinak. Soubory GRAPH.TPU a GRAPH.TPP tu mají úlohu jenom jakéhosi zprostředkovatele. To hlavní totiž vykonávají grafické ovladače BGI.
Součástí instalace Turbo pascalu je ovladač pro 16 barevné VGA módy a dále několik dalších ovladačů pro dnes již historické grafické karty, se kterými se už nesetkáte.
Poslední verze turbo pascalu, Borland pascal 7 přidává ještě ovladače pro 16 barevné SVGA módy, ale to je vše. Na internetu se ale dají naštěstí stáhnout BGI ovladače pro 256 barevné režimy a dokonce i pro HighColor a TrueColor.
Tenhle článek měl původně být propagace BGI grafiky. Jenže během psaní doprovodného příkladu vypluly na povrch hrozivé bugy nových BGI ovladačů, které jsou tak závažné, že je dělají téměř nepoužitelnými.
Hlavním tématem měly být operace GetImage a PutImage.
GetImage zkopíruje do bufferu v paměti pravoúhlý výřez obrazovky.
PutImage dělá opak. Buffer naplněný předchozím voláním procedury GetImage zkopíruje zpět na obrazovku.
Podívejme se na deklaraci procedury PutPixel:
Procedure PutPixel(x,y:integer; var bitmapa; jak:word);  
Parametry X a Y jsou jasné - to je poloha na obrazovce na kterou se výřez vykreslí (označuje levý horní roh).
Bitmapa je proměnná, ve které je výřez uložen. (Jde o tzv. beztypovou proměnnou. Ve většině případů na ni napasujete typ pointer)
Jak označuje způsob, jakým se výřez vykreslí. A tady to začíná být zajímavé! BGI ovladače by měly umět tyto čtyři způsoby vykreslení:
0COPYput, Normalputpřemaluje to, co je na obrazovce (na pozadí)
1XORputNonekvivalence výřezu a pozadí
2ORputLogický součet výřezu a pozadí
3ANDputLogický součin výřezu a pozadí
4NOTputNegace
Až si budete jednotlivé kreslicí režimy zkoušet, tak zjistíte, že jediný použitelný je COPYput. Ostatní divně deformují pozadí. Jenže COPYput nerozlišuje prázdné body. Když máte zkrátka objekt s dírou nebo s jinými prázdnými body, které byste rádi chápali jako průhledné, tak COPYput vykreslí i je. Každý objekt proto vypadá, jako by byl uvnitř černého obdélníku. Zkrátka to vypadá hnusně.
Myslím si, že tohle je první důvod, proč programátoři přestávají používat unit Graph a začínají hledat jiné cesty, jak pracovat s grafikou.
My si ale ukážeme, že transparentní PutImage se s využitím triku dá udělat pomocí jednotky Graph také.

Základem myšlenky je skutečnost, že pokud máme prázdné pozadí, tak místo COPYput se dá použít i ORput. Je to samozřejmé - logický součet X+0 bude vždycky X.
Problém je v tom, že pozadí obvykle prázdná nebývají. Otázkou tedy je, jak ho vymazat. A tady nám zase pomůže operace ANDput.
Jenže když jen tak bez přípravy váš výřez pomocí ANDput zkopírujete na obrazovku, tak ji vymažete dočista, takže také vznike černý obdélník. Tudíž vaše sekvence PutImage(x,y,obrazek^,ANDput);PutImage(x,y,obrazek^,ORput); bude ve výsledku vypadat stějně jako staré známé PutImage(x,y,obrazek^,COPYput);
Takže?
Fígl je v tom, že si připravíme tzv. masku.
Připravenou masku vykreslíme pomocí operace ANDput a potom vykreslíme výřez pomocí ORput.

Ale jak tu masku uděláme?
Uvědomme si, jak pracuje operace AND. Máte? Vytvoříme si další buffer, který má shodnou velikost jako náš výřez. A teď budeme procházet výřez pixel po pixelu a budeme testovat, zdali je daný pixel roven 0.
*Pokud ano, tak na odpovídající místo masky dáme nejvyšší možnou hodnotu pixelu. (tedy v 256 barevných režimech to bude 255, v HighColor 65535 atd)
*Pokud ne, tak na odpovídající místo masky dáme hodnotu 0.
Tímto jsme tedy vytvořili "dvoubarevnou" masku, kde platí, že body s hodnotou 0 je popředí a ty druhé jsou pozadí.

V dále uvedeném zdrojáku se podívejte na proceduru VytvorMasku.
Vidíte, že to není nic složitého. Jenom je potřeba znát formát dat procedur GetImage/PutImage pro použitý videorežim. V TP si dejte pozor na to, že 0. a 1. bajt výřezu označují jeho šířku a 2. a 3. jeho výšku. Až potom následuje vlastní bitmapa. Ve FP jsou šířka a výška určeny 4+4 bajty.
Tím jsme odbyli problematiku transparentního PutImage.
Jak jste si už asi všimli, tak zdroják je poněkud složitější. Původně jsem totiž chtěl ukázat, jak dosáhnout plynulé animace pomocí přepínání stránek videopaměti. Jenže tady se právě projevily bugy BGI ovladačů. o kterých jsem se zmiňoval výše. Zkoušel jsem dva BGI ovladače pro 256 barevné SVGA módy a oba mají hrozné chyby v přepínání obrazových stránek. Další bug jsem objevil v clippingu procedury PutImage, takže konečnou verzi jsem musel udělat tak, aby nedocházelo k vylézání z obrazovky.
Freepascalovský unit Graph však naštěstí tyto problémy nemá. Díkybohu!

uses Crt,Graph;


Procedure NakresliPodklad;
{Necim zaplacam pozadi}
var x,y:longint;
begin
for y:=0 to GetMaxY do
    for x:=0 to GetMaxX do PutPixel(x,y,byte(x*y));
end;


Procedure NakresliSprajt;
const Mnohouhelnik:array[1..5] of PointType =
   (
   (X:75; Y:0),(X:125; Y:160),(X:25; Y:30),(X:125; Y:30),(X:35; Y:160)
   );
begin
SetFillStyle(1,14);
SetColor(14);
FillPoly(5,mnohouhelnik);
end;


Procedure UlozSprajt(var p:pointer;var v,si,vy:word);
begin
v:=ImageSize(25,0,125,160);
GetMem(p,v);
GetImage(25,0,125,160,p^);
si:=125-25+1;
vy:=160-0+1;
end;


Procedure BufferProPozadi(var pozadi:pointer;velikost:word);
begin
GetMem(pozadi,velikost);
end;


Procedure VytvorMasku(var s,m:pointer;v:word);
var ss,mm:^byte;
    w:word;
    h:byte;
begin
{$IFDEF FPC}h:=8;{$ELSE}h:=4;{$ENDIF}
GetMem(m,v);         {maska je stejne velika jako samotny sprajt}
Move(s^,m^,h);
ss:=s;         {namirime ukazatele na zacatek sprajtu}
mm:=m;         {i na zacatek masky}
inc(ss,h);        {preskocime "zahlavi"}
inc(mm,h);        {--"--}
for w:=1 to v-h do      {pro kazdy pixel sprajtu...}
    begin
    if ss^=0 then
       begin
       mm^:=255
       end else
       begin
       mm^:=0;
       end; {na misto odpovidajici dire dej 255,}
    inc(ss);                            {jinak 0}
    inc(mm);
    end;
end;


Procedure CekejNaPaprsek;assembler;
asm
 Mov dx,3DAh
 @l1:
 in al,dx
 and al,08h
 jnz @l1
 @l2:
 in al,dx
 and al,08h
 jz @l2
end;




var gd,gm:integer;
    sprajt:pointer;
    maska:pointer;
    {pozadi:pointer;}
    pozadi:array[0..1] of pointer;
    velikost,si{rka},vy{ska}:word;
    xp,yp:integer;
    deltaX,deltaY:integer;
    _xp,_yp:array[0..1] of integer;
    b:longint;


begin
{$IFDEF FPC}
gd:=d8bit;
gm:=m640x480;
InitGraph(gd,gm,'');
{$ELSE}
gd:=InstallUserDriver('SVGA256',nil); {pro gr. operace bude vyuzivat soubor SVGA256.BGI}
gm:=2;    {640x480 256 barev}
InitGraph(gd,gm,'');
{$ENDIF}




NakresliSprajt;
UlozSprajt(sprajt,velikost,si,vy);  {tento sprajt ulozim do pameti}
VytvorMasku(sprajt,maska,velikost); {vytvorim jeho bitovou masku}
BufferProPozadi(pozadi[0],velikost);   {a pripravim si buffer na ukladani pozadi}
BufferProPozadi(pozadi[1],velikost);   {budu je ale potrebovat dva, protoze stridave}
                                       {zobrazuju videostranku 0 a 1}
NakresliPodklad;


{$IFDEF FPC}
SetActivePage(1);
NakresliPodklad;
SetActivePage(0);
{$ENDIF}


xp:=90;
yp:=90;
_xp[0]:=xp;
_yp[0]:=yp;
_xp[1]:=xp;
_yp[1]:=yp;
deltaX:=5;
deltaY:=5;
b:=0;


GetImage(xp,yp,xp+si-1,yp+vy-1,pozadi[0]^);
GetImage(xp,yp,xp+si-1,yp+vy-1,pozadi[1]^);
repeat


{$IFDEF FPC}
SetVisualPage(longint(b xor 1)); {vzdy se koukam na jinou videostranku nez na tu}
SetActivePage(longint(b));     {kterou prave menim}
CekejNaPaprsek;      {Silne doporucuju po zmene videostranky pockat na navrat paprsku}
{$ENDIF}             {Pokud cekani vypustite, tak zacne animace blikat!}


{ 1. krok - obnova pozadi}
PutImage(_xp[b],_yp[b],pozadi[b]^,COPYput);


{ 2. krok - ulozeni pozadi}
GetImage(xp,yp,xp+si-1,yp+vy-1,pozadi[longint(b)]^);


{ 3. krok - vycisteni pozadi pomoci masky (schvalne to zkuste vynechat :-) }
PutImage(xp,yp,maska^,ANDput);


{ 4. krok - dokresleni sprajtu na vycistene pozadi }
PutImage(xp,yp,sprajt^,ORput);


_xp[b]:=xp; {minulou pozici musim uchovavat pro kazdou}
_yp[b]:=yp; {videostranku zvlast}


inc(xp,deltaX);
inc(yp,deltaY);
if xp+si>GetMaxX then begin deltaX:=-deltaX;inc(xp,deltaX);end else
   if xp<0 then begin deltaX:=-deltaX;inc(xp,deltaX);end;
if yp+vy>GetMaxY then begin deltaY:=-deltaY;inc(yp,deltaY);end else
   if yp<0 then begin deltaY:=-deltaY;inc(yp,deltaY);end;


{$IFDEF FPC}
b:=b xor 1; {Prepinac zobrazovanych stranek}
{$ENDIF}


until keypressed;
while keypressed do readkey;


CloseGraph;
end.  
Pokud nemáte soubor SVGA256.BGI, tak si ho můžete stáhnout zde (verze z roku 1994 od Jordana Hargraphixe).

Základní myšlenkou přepínání stránek je to, že vždy měníme tu stránku, která není zrovna vidět. Např. koukáme na stránku 0 a mezitím něco děláme na stránce 1. Jakmile se to dodělá, tak se přepneme na stránku 1 a začneme měnit stránku 0. Tímto mechanizmem můžeme obejít další závažný nedostatek unitu Graph. Mám na mysli to, že nedovoluje kreslit do virtuálních obrazovek. To platí pro TP i pro FP verzi. Stránkováním se to ale dá solidně nahradit. Jenom je to hrozná škoda, že kvůli hloupým chybám to nejde použít v TP. Nevzdávám se ale naděje, že existují i nějaké lepší, nezabugované BGI ovladače. Doufám.
Jistou nevýhodou mého postupu je to, že jsou potřeba dva buffery na uchovávání přepsaných částí obrazovky, ale myslím, že to není velká daň.

Oba postupy, o kterých tu píšu, tedy PutImage pomocí AND/OR a stránkování videopaměti mají široké použití i mimo jednotku Graph. U stránkování je to mimo jakoukoliv pochybnost. Díky stránkování můžete odbourat některá zdlouhavá kopírování virtuálních obrazovek na monitor.
A AND/OR putimage je taky skvělá věc. Vykreslování transparentních sprajtů je totiž zcela zásadní záležitost, bez které se neobejdeme. Drtivá většina programátorů ale používá postup s explicitním testováním transparence. Tedy něco takového:
{ESI ukazuje na sprajt, EDI na obrazovku}
@dalsi:
     mov al,[esi]
     cmp al,0
  je @preskoc
     mov [edi],al
@preskoc:
     inc esi
     inc edi
     dec ecx
  jnz @dalsi
    

Řekněme, že tento kus zdrojáku zpracovává obrázek v 256 barvách. Vidíte, že rutina zpracovává obrázek po jednom bajtu. Žádné 32-bitové zpracování, natož MMX. A navíc ten skok. Skoky dost podstatně zpomalují průběh algoritmu, protože rozhodí mechanizmus předpovídání instrukcí a cache.

Pro HighColor a TrueColor režimy na procesorech Pentium Pro a novějších by byla zajímavá varianta využití instrukce cmove
(bohužel se nedá použít pro 8-bitové přesuny a pro přesuny registr/paměť a paměť/registr)
@dalsi:
     mov ax,[esi]
     mov bx,[edi]
     cmp ax,0
     cmove ax,bx
     mov [edi],ax
     inc esi
     inc edi
     dec ecx
  jnz @dalsi  


Když ale použijeme postup AND/OR, tak můžeme zpracovávat 32-bitově a otvíráme si dokonce cestu k instrukcím MMX.
{ESI ukazuje na sprajt, EBX na masku, EDI na obrazovku}
@dalsi:
     mov edx,ds:[ebx]
     and [edi],edx
     mov edx,[esi]
     or  [edi],edx
     inc esi
     inc edi
     inc ebx
     dec ecx
  jnz @dalsi  
2006-12-06 | Laaca
Reklamy: