Demoskolan del3, sprites

Hej och välkomna till del 3 i Albert's demoskola. Jag har fått vissa klagomål på förra delen, bl a att den var stökigt skriven och att det var för svårt att hänga med och förstå någonting. Därför tänkte jag vänta med 3D vektorgrafik till nästa del och gå igenom nått lite mer lättsmält den här gången, nämligen Sprites och hur man får upp en TGA bild på skärmen. Dessutom ska jag gå igenom hur man skriver funktioner i ren assembler i en separat fil och hur man anropar dessa funktioner från C-programmet.

VAD ÄR EN SPRITE?

En sprite är en liten rektangulär, punktuppbyggd bild som kan föreställa t ex en boll eller en gubbe i ett spel. I stort sett alla spel innehåller sprites, i ett schackspel brukar pjäserna vara sprites (om det inte är 3D). I DOOM är alla monster sprites och i bilspel brukar bilarna vara sprites. Ett objekt behöver inte bestå av bara en sprite. I ett bilspel där man ser bilarna uppifrån (typ micromachines) kan varje bil bestå av 4 sprites. En där bilen är vänd uppåt, en där bilen är vänd åt vänster, en nedåt och en åt höger. Sen är det upp till programmet och avgöra åt vilket håll bilen är vänd och visa rätt sprite. I fallet med bilspelet borde man ha minst 8, helst 16 sprites för att kunna visa bilen i flera riktningar och få mjukare svängar.

De flesta datorer har idag hårdvarustöd för att rita ut sprites (på Amigan fanns det redan för 10 år sedan), bitblt brukar det kallas och det står för bit blast och betyder ungefär punktbombardemang. På PC'n blir det dock lite mera komplicerat. De flesta grafikorten till pc har stöd för bitblt men tyvärr finns det ingen standard så det är ingen idé att använda sig av bitblt eftersom det bara kommer att fungera på de datorer som har samma grafikkort som ens egen dator. Men om någon är intresserad av att försöka så finns det en grundlig genomgång av de flesta nu existerande grafikkorten till pc i boken "Programmers Guide to the Ega, Vga and SuperVga Cards" av Richard Ferraro.

Eftersom det inte finns någon standard så får vi själva skriva sprite rutinerna. För att få rutinerna så snabba som möjligt har jag använt 32-bitars instruktioner (movsd) som flyttar 32 bitar i taget vilket är 4 bytes. Det betyder att varje sprite i, x-led, måste ha ett, jämnt delbart med 4, antal punkter. En sprite kan alltså vara t ex 16, 20, 24, 28 eller 32 punkter bred. Höjden kan vara mellan 1 och 199 punkter. Eftersom vi använder MCGA (mod 13h) så finns det 256 färger vilket gör att varje punkt i spriten blir en byte.

Jag lagrar spriten så här:
Först 2 bytes (en integer) för höjden på spriten och sedan 2 bytes för bredden och sedan en byte för varje pixel i spriten. Det brukar kallas för rådataformat.

VARNING vissa rådatafiler har bredden först sen höjden, men jag har höjden först eftersom det passar spriterutinerna bättre. EX, en sprite som är 20 pixels hög och 16 pixels bred blir:

Höjd   Bredd
0, 20, 0, 16, 

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0         Rad 1 16 bytes
0,0,0,0,0,34,56,6,6,56,34,0,0,0,0,0     Rad 2 16 bytes
                                        .
O S V . . . i 18 rader till             .
Det är dock ganska jobbigt att skriva in alla spritar för hand. Jag brukar rita spritarna i något rit-program (t ex 3d studio) och spara dem i TGA format. Sedan översätter jag från tga-format till mitt eget sprite format med hjälp av ett litet c-program (tga2spr.c) som följer med demo3.zip. tga2spr klarar bara av 256-färgers tga-bilder men det går att konvertera true-color tga till 256 färgers tga i de flesta ritprogram (t ex Corel Photopaint eller Fauve Matisse).

Jag har gjort några sprite rutiner:

Loadsprite laddar in spriten med namnet Namn till Sprite. Sprite är en array som man får se till är tillräckligt stor för att rymma hela spriten. Ex: char Sprite[2 + MAXBREDD*MAXHOJD];
loadsprite("minsprite.spr", Sprite);

show32spr kopierar spriten Sprite till x,y i destinationen med hjälp av 32bits instruktionen movsd.

Ex: screen=0x0a0000000;
show32spr(10,20, Sprite, screen);

kopierar Sprite med övre vänstra hörnet i (10,20) på skärmen, screen är en pekare till skärmen (0x0a000:0000).

Showtrans visar spriten med färg 0 genomskinlig. Om bakgrunden inte är svart och spriten är svart i kanterna används alltid showtrans. Showtrans är långsammare än show32spr så om bakgrunden är svart används show32spr med fördel.

Sudda32bit sätter ett område, vid (x,y) med bredden bredd och höjden hojd, i dest till svart. Sudda32bit används för att sudda ut en sprite om bakgrunden är svart. Om bakgrunden inte är svart får man först klippa ut bakgrunden med get32spr innan man ritar ut spriten och för att sudda spriten ritar man sedan tillbaka bakgrunden med show32spr.

Get32spr klipper alltså ut bakgrunden vid x, y i source och placerar resultatet i dest. Source är lämpligen skärmen eller en virtuell skärm med bredden 320 och dest är en sprite.

 !OBS  Om man använder en virtuell skärm måste den vara 320 bytes bred  OBS!
 !OBS           eftersom alla spritefunktioner antar det                OBS!
Alla spritefunktioner finns i d3asm.asm och är kommenterade.

HUR VISAS FLERA SPRITES SAMTIDIGT?

Problemet är att det bara finns en palett. Om sprite 1 har 100 färger och sprite 2 har 80 färger går det naturligtvis att passa in båda spritarnas färger i samma palett, eftersom antalet färger är mindre än 256. Men om man vill ha en bakgrundsbild samtidigt och mer än 2 sprites då?

Det finns naturligtvis många sätt att göra detta, jag valde att göra en palett först. Sedan skrev jag c-program som anpassade tga-bilder och sprites till paletten. Programmet läser först in en bild med palett och sedan läses paletten som bilden ska anpassas till in. Programmet jämför de båda paletterna och bestämmer vilka färger i palett 2 som är närmast färgerna i palett 1. På så sätt räknas en utbytes array ut. Sedan byts punkterna i bilden ut enligt utbytesarrayen och den nya anpassade bilden sparas under ett nytt namn. C-filerna heter passtga.c och passpr.c.

Passtga läser en 256-färgers tga bild och en palett och sparar rådata, samma som spritarna. Höjden, bredden och sedan data pixel för pixel. Passpr.c fungerar likadant fast den läser in en sprite och två paletter, eftersom en sprites palett ligger lagrad som en separat fil.

Passtga och Passpr använder jag själv och de är inte särskilt användarvänliga. Man måste gå in i main och skriva in namnen på filerna man vill manipulera. Det syns ganska tydligt var man ska ändra, det är överst i main. Passtga och passpr är inte särskilt välskrivna heller. De är ett typiskt resultat av hackande. Jag satte mig vid datorn och började planlöst hacka tills jag fick fram nåt som funkade vilket resulterade i ett fungerande med oerhört virrigt program. Jag orkar inte gå igenom det och fixa till det eftersom det ju trots allt fungerar i alla fall.
Se filen targa.txt för en tydligare beskrivning av targaformatet.

ATT KOMBINERA C OCH ASSEMBLER

Spritefunktionerna är alltså skrivna i assembler men huvudprogrammet är skrivet i C, hur går det till?
Tänk först på hur det går till när man kompilerar ett vanligt c-program. Om man använder t ex Borland C trycker man bara på Ctrl F9 och så sker allt automatiskt. Men vad kompilatorn gör är faktiskt att först kompilera c-koden till en objektfil med ändelsen .obj. Sedan länkas objektkoden till en körbar .exe fil. Om man assemblerar en .asm fil med t ex tasm så blir resultatet en .obj fil. Sedan länkar man .obj filen med tlink till en .exe fil.

För att kombinera en fil som är skriven i assembler och en som är skriven i C så assemblerar man assemblerfilen till en .obj fil och man kompilerar C-filen till en .obj fil. Till sist länkar man ihop assemblerfilens .obj fil och C-filens .obj fil till en gemensam .exe fil.

En funktion som är skriven i assembler anropas precis likadant som en funktion som är skriven i C. Överst i C programmet talar man om vilka funktioner som är skrivna utanför C programmet med C kommandot extern.

Ex, antag att wtsync() är skriven i assembler i en annan fil. Då blir C-programmet så här:

#include               /* ta inte med stdio om det inte behövs */
                                /* det ökar bara storleken på .exe filen */

extern void wtsync(void);       /* denna funktion finns nånannanstans */

void main(void)
{
        .
        .
        .
        wtsync();               /* wtsync anropas precis som vanligt */
        .
        .
        .
}
Och assemblerfilen blir:
        
        P386N                   ;tillåt 386 instruktioner (movsd tex)
        IDEAL                   ;använd tasm's ideal mode
        MODEL small             ; (data < 64k) och (kod < 64k)
        CODESEG

        PUBLIC  _wtsync         ;denna rutin ska kunna anropas från C

PROC    _wtsync NEAR

        .                       ;här emellan är koden för wtsync
        .
        .
        
        ret
ENDP    _wtsync

        END
Observera att wtsync börjar med ett _ i assemblerfilen. Detta är standard, man lägger alltid till ett _ före funktionsnamnet när funktionen ska kunna anropas från c. Antag att wtsync är skriven i filen testasm.asm och att C-programmet heter testc.c. För att bygga exe-filen kan man gå till väga på olika sätt. Jag brukar använd tasm för assemblerfilen och borland C++ 3.1 command line compiler, bcc för c-filen:

Med Borland C++ skriv:
        tasm /ml testasm.asm
        bcc -c testc.c
        bcc -ms testc.obj testasm.obj

Med Turbo C++ skriv:
        tasm /ml testasm.asm
        tcc -c testc.c
        tcc -ms testc.obj testasm.obj
tasm /ml tillverkar assemblerfilens .obj fil. /ml betyder att tasm ska göra skillnad på stora och små bokstäver (case sensitive på engelska).

bcc -c tillverkar C-filens .obj fil. -c betyder endast kompilering, ingen länkning.

bcc -ms länkar objektfilerna till en körbar .exe-fil. -ms betyder att kompilatorn ska använda minnesmodell small. Resultatet blir en fil som heter testc.exe eftersom testc.obj stod först vid länkningen.

Svårare än så är det inte. Det finns en himla massa fördelar med att skriva vissa funktioner i ren assembler och länka ihop dem med C-programmet. Bland annat så kan man i Borland C++ 3.1 inte använda sig av 386-instruktioner som t ex movsd, men det går utmärkt att använda 386-instruktioner i ren assembler. Dessutom blir C programmet mycket mindre och mer läsbart om man delar upp programmet i flera mindre filer, det minskar dessutom risken för fel och det blir lättare att isolera fel när man debuggar. Svårigheten är att skriva assemblerfunktionerna, jag har lärt mig det mesta jag kan genom att läsa den utmärkta boken Mastering Turbo Assembler av Tom Swan nu i sommar.

Några saker att tänka på:

* Skicka argument från c till assembler: Använd ARG direktivet se d3asm.asm för exempel. Här är skalet för en rutin som tar argument:

                ARG     xpos:word, . . .        ;skriv alla argument här
                push    bp              ;detta har med stacken att göra
                mov     bp,sp           ;bry dig inte om det, skriv bara dit
                .                       ;push bp och mov bp,sp först i rutinen
                .                       ;och pop bp sist i rutinen
                .                       ;om du använder argument
                .
                mov     ax,[xpos]       ;använd argumentet
                .
                .
                .
                pop     bp
                ret
* Att skicka tillbaka returvärden från assemblerrutinen till c-programmet:
        Returvärden skickas i ax och dx beroende på storlek

                Storlek         returregister   Exempel på datatyp
                Byte            al              char
                Word (2 byte)   ax              int
                Dword (4 byte)  dx:ax           char far*

* Spara alltid undan ds, si och di på stacken om du ändrar dessa i rutinen annars är sannolikheten stor att programmet hänger sig eftersom C-programmet annvänder sig av ds, si och di. Ax, bx, cx, dx och es kan man ändra fritt på, det spelar ingen roll för c-programmet.

* undvik lokala variabler, använd register i så stor utsträckning som möjligt. Register är många gånger snabbare än lokala variabler. Om du måste använda lokala variabler använd assemblerdirektivet LOCAL. LOCAL fungerar ungefär som ARG.

Jag har börjat använda ideal mode på senaste tiden. Det är tasm's egen dialekt av pc assembler. Ideal mode innehåller några förenklingar som gör koden enklare att förstå. Skillnaderna är ganska små, t ex skriver man codeseg istället för .code och lite andra småsaker. Ute på internet är ideal mode ganska stort, åtminstone när det gäller demokodning, dessutom använder sig Tom Swan av ideal mode i Mastering Turbo Assembler.

EXEMPELPROGRAMMET DEMO3.C

Alla funktioner som demo3 använder sig av är skrivna i assembler i filen d3asm.asm. C-programmet är ganska självförklarande, det är dock några av funktionerna som kan behöva lite närmare förklaring. Showpic läser in en bild, som är lagrad som rådata, till skärmen eller en virtuell skärm. Jag tillverkade bilden med hjälp av passtga.c. Programmet använder sig av en virtuell skärm som är lika stor som den riktiga skärmen (64000 bytes). Först allokeras minne till skärmen mha malloc (som finns i d3asm.asm). Om det gick bra laddas en bild in till den virtuella skärmen och en paletten laddas från hårddisken och sätts. Sedan flippas bilden över från den virtuella skärmen till den fysiska skärmen med en dissolve effekt. Dissolve effekten använder sig av en ganska intressant algoritm som ger alla tal från 1 - FFFFh i en slumpmässig ordning, inget tal kommer mer än en gång.
PSEUDOKOD:

        
        sätt offset till ett tal skilt från noll (t ex 1)
        for(i=0;i<0x0FFFF;i++)
        {
                gör något nyttigt med offseten
                .
                .
                .
                shifta offseten en bit åt höger (som att dela med 2)
                om den utshiftade biten var en etta
                sätt offset till offset xor 0x0b400
          }
på detta sätt kommer offset att anta alla värden mellan 1 - ffffh. Fråga mig inte hur det går till men det fungerar faktiskt. Jag fick idén från Graphic Gems I sid 221.

Efter dissolve effekten kommer huvudloopen där en genomskinlig sprite studsar över skärmen. Allt ritande och klippande sker i den virtuella skärmen. När ritandet är klart flippas den virtuella skärmen över till den fysiska skärmen och nästa bild börjar ritas. Huvudloopen gör följande:

        
        1.      Klipp ut bakrunden där spriten ska ritas ut
        2.      Rita ut spriten
        3.      Flippa över virt till den fysiska skärmen
        4.      sudda genom att rita tillbaka bakgrunden över spriten
        5.      räkna ut nya koordinater för spriten och hoppa till 1
Innan programmet avslutas måste man lämna tillbaka det allokerade minnesutrymmet med hjälp av funktionen free (som också finns i d3asm.asm)

Se filen targa.txtför beskrivning av targaformatet.

Nästa del kommer att handla om 2d och 3d vektorgrafik. Jag håller på att översätta rutinerna från c till assembler och det tar sin lilla tid. Men ni kommer att få rutinerna serverade på fat, färdiga att använda och göra egna 3d-demos med. Den klurigaste rutinen är den som ritar ut en fylld triangel. Jag funderar över vilket sätt som är det absolut snabbaste att rita ut en fylld triangel. Om någon vet så hör gärna av er till mig.

Ladda hem demoskolan del3

Ta det lugnt med spriten


halsar Abe Raham

s-mail: Albert Veli
        Spisringsg. 9
        724 76 Vdsteres
mail:dat94avi@bilbo.mdh.se.

Tillbaka till min hemsida.