VGA, Przewijanie ekranu, dwie warstwy

LOSMARCELOS

Techniki przewijania "paralaktycznego"
Co to jest zjawisko paralaksy ? Doświadczemy tego jak jedziemy samochodem. Obiekty bliżej przesuwają się szybciej, dalsze... wolniej.

Można taki efekt zrobić przy pomocy C, assemblera i trybu 13 h

user image

O to przykładowy kod w C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <dos.h>
#include "paralx.h"

    char *MemBuf,            // wskaźnik na bufor
         *BackGroundBmp,     // wskaźnik na bitmape tła
         *ForeGroundBmp,     // wskaźnik na bitmapę pierwszego planu
         *VideoRam;          // wskaźnik na pamięć video VGA

    PcxFile pcx;             // struktura danych pliku typu PCX

    int volatile KeyScan;    // modyfikowane przez przerwanie klawiatury

    int frames=0,            // liczba wyświetlonych klatek
        PrevMode;            // przechowuje oryginalny tryb video

    int background,  // znacznik podziału tła bitmapy
        foreground,  // znacznik podziału bitmapy pierwszoplanowej
        position;    // całkowita odległość przewijania

    void _interrupt (*OldInt9)(void); // // wskaźnik na procedure obsługi przerwania klawiatury systemu BIOS

//
//  Ta funkcja ładuje plik PCX
//
    int ReadPcxFile(char *filename,PcxFile *pcx)
    {
      long i;
      int mode=NORMAL,nbytes;
      char abyte,*p;
      FILE *f;

      f=fopen(filename,"rb");
      if(f==NULL)
        return PCX_NOFILE;
      fread(&pcx->hdr,sizeof(PcxHeader),1,f);
      pcx->width=1+pcx->hdr.xmax-pcx->hdr.xmin;
      pcx->height=1+pcx->hdr.ymax-pcx->hdr.ymin;
      pcx->imagebytes=(unsigned int)(pcx->width*pcx->height);
      if(pcx->imagebytes > PCX_MAX_SIZE)
        return PCX_TOOBIG;
      pcx->bitmap=(char*)malloc(pcx->imagebytes);
      if(pcx->bitmap == NULL)
        return PCX_NOMEM;

      p=pcx->bitmap;
      for(i=0;i<pcx->imagebytes;i++)
      {
        if(mode == NORMAL)
        {
          abyte=fgetc(f);
          if((unsigned char)abyte > 0xbf)
          {
            nbytes=abyte & 0x3f;
            abyte=fgetc(f);
            if(--nbytes > 0)
              mode=RLE;
          }
        }
        else if(--nbytes == 0)
          mode=NORMAL;
        *p++=abyte;
      }

      fseek(f,-768L,SEEK_END);      // odczytanie palety kolorów
      fread(pcx->pal,768,1,f);
      p=pcx->pal;
      for(i=0;i<768;i++)            // przesunięcie bitów palety
        *p++=*p >>2;
      fclose(f);
      return PCX_OK;                // zwróć wynik funkcji
    }

//
// To jest nowa procedura obsługi przerwania 9h.
// W przypadku niewyłączenia obsługi przerwania systemu BIOS przytrzymanie klawisza
// powoduje przepełnienie bufora klawiatury i bardzo denerwujące pipczenie. 
//
//     void _(podkreślnik)interrupt NewInt9(void) w niektórych kompilatorach C/C++
    void _interrupt NewInt9(void)
    {
      register char x;

      KeyScan=inp(0x60);       // odczytanie kodu klawisza
      x=inp(0x61);             // poinformowanie klawiatury, że naciśnięto klawisz
      outp(0x61,(x|0x80));
      outp(0x61,x);
      outp(0x20,0x20);                  // wysyłanie kodu End-Of-Interrupt
      if(KeyScan == RIGHT_ARROW_REL ||  // sprawdzenie kodu klawisza
         KeyScan == LEFT_ARROW_REL)
         KeyScan=0;
    }

//
//  Ta funkcja odtwarza oryginalną obsługę przerwania klawiatury
//
    void RestoreKeyboard(void)
    {
      _dos_setvect(KEYBOARD,OldInt9);
    }

//
//  Ta funkcja zapamiętuje oryginalną obsługę przerwania klawiatury i instaluje nową
//
    void InitKeyboard(void)
    {
      OldInt9=_dos_getvect(KEYBOARD);   // zapamiętanie oryginału
      _dos_setvect(KEYBOARD,NewInt9);   // zainstalowanie nowej obsługi
    }

//
// Ta funkcja wywołuje system BIOS do ustawienia wszystkich rejestrów karty VGA
// na podstawie zawartości tablicy PAL
//
    void SetAllRgbPalette(char *pal)
    {
      struct SREGS s;
      union REGS r;

      segread(&s);                    // odczytanie wartości aktualnego segmentu
      s.es=FP_SEG((void far*)pal);    // ustawienie rejestru na tablice pal
      r.x.dx=FP_OFF((void far*)pal);  // odczyt przesunięcia tablicy pal
      r.x.ax=0x1012;                  // funkcja 10h, podfunkcja 12h systemu BIOS
      r.x.bx=0;                       // początkowy rejestr
      r.x.cx=256;                     // końcowy rejestr
      int86x(0x10,&r,&r,&s);          // wywołanie systemu BIOS
    }

//
// Ta funkcja ustawia tryb 13h
//
    void InitVideo()
    {
      union REGS r;

      r.h.ah=0x0f;               // funkcja systemu BIOS 0fh
      int86(0x10,&r,&r);         // wywołanie systemu BIOS
      PrevMode=r.h.al;           // zapamiętanie poprzedniego trybu
      r.x.ax=0x13;               // ustawienie trybu 13h : 320X200X256
      int86(0x10,&r,&r);         // wywołanie systemu BIOS
      VideoRam=MK_FP(0xa000,0);  // utworzenie wskaźnika na pamięci VIDEO
    }

//
// Ta funkjca przywraca poprzedni tryb VIDEO
//
    void RestoreVideo()
    {

      union REGS r;

      r.x.ax=PrevMode;          // odtworzenie poprzedniego trybu
      int86(0x10,&r,&r);        // wywołanie systemu BIOS
    }


//
// Ta funkjca ładuje warstwy
//
    int InitBitmaps()
    {
      int r;

      background=foreground=1;                // punkt początkowego podziału

      r=ReadPcxFile("backgrnd.pcx",&pcx);     // odczytanie bitmapy tła
      if(r != PCX_OK)                         // sprawdź czy nie było błędów
        return FALSE;
      BackGroundBmp=pcx.bitmap;               // zapamiętanie wskaźnika na bitmape
      SetAllRgbPalette(pcx.pal);              // ustaw palete VGA

      r=ReadPcxFile("foregrnd.pcx",&pcx);     // odczytanie bitmapy pierwszego planu
      if(r != PCX_OK)                         // sprawdź czy nie było błędów
        return FALSE;
      ForeGroundBmp=pcx.bitmap;               // zapisz wskaźnik bitmapy

      MemBuf=malloc(MEMBLK);                  // utworzenie bufora
      if(MemBuf == NULL)                      // sprawdź czy nie było błędów
        return FALSE;

      memset(MemBuf,0,MEMBLK);                // wyczyść bufor
      return TRUE;                            // Wszystko OK ! :-)
    }


//
//  Ta funkcja zwalnia całą pamięć przydzieloną przez program
//
    void FreeMem()
    {
      free(MemBuf);
      free(BackGroundBmp);
      free(ForeGroundBmp);
    }

//
// Ta funkcja rysuje warstwy.
// Porządek jest wyznaczany na podstawie wartości współrzędnej "z"
//
    void DrawLayers()
    {
      OpaqueBlt(BackGroundBmp,0,100,background);
      TransparentBlt(ForeGroundBmp,50,100,foreground);
    }

//
// Ta funkcja obsługuje animację. Proszę zauważyć, że jest to najbardziej krytyczna część kodu.
// Aby ją zoptymalizować należało by przepisać ją w asemblerze
//
    void AnimLoop()
    {
      while(KeyScan != ESC_PRESSED)       // powtarzaj dopóki nie naciśnięto ESC
      {
        switch(KeyScan)                   //
        {
        case RIGHT_ARROW_PRESSED:         // naciśnięto strzałkę w prawo
          position--;                     //
          if(position < 0)                // jeśli doszliśmy do końca to przerwij przewijanie
          {
            position=0;
            break;
          }
          background-=1;                  // przewijanie tła o 2 piksele
          if(background < 1)              // czy osiągnęliśmy koniec ?
            background+=VIEW_WIDTH;       // ...to "zawijamy"

          foreground-=2;                  // przewijanie planu pierwszego o 4 piksele
          if(foreground < 1)              // czy osiągneliśmy koniec ?
            foreground+=VIEW_WIDTH;       // ... jeśli tak to "zawijamy" 

          break;
        case LEFT_ARROW_PRESSED:          // naciśnięto strzałkę w lewo
          position++;                     //
          if(position > TOTAL_SCROLL)     // zatrzymaj przewijanie jeśli doszliśmy do końca
          {
            position=TOTAL_SCROLL;
            break;
          }
          background+=1;                  // przewijanie tła o 2 piksele
          if(background > VIEW_WIDTH-1)   // czy osiągneliśmy krawedź ekranu ?
            background-=VIEW_WIDTH;       // ...to "zawijamy"

          foreground+=2;                  // przewijanie planu pierwszego o 4 piksele
          if(foreground > VIEW_WIDTH-1)   // czy osiągneliśmy krawędź ekranu ?
            foreground-=VIEW_WIDTH;       // ... to "zawijamy"

          break;
        default:                          //
          break;
        }
        DrawLayers();                    // narysowanie warstw w buforze
        memcpy(VideoRam,MemBuf,MEMBLK);  // skopiowanie zawartości bufora do pamięci video
        frames++;                        // liczba wyświetlonych klatek
      }
    }

//
//  Ta funkjca dokonuje inicjalizacji
//
    void Initialize()
    {
      position=0;
      InitVideo();            // ustaw tryb 13h
      InitKeyboard();         // zainstaluj naszą "obsługe klawiatury"
      if(!InitBitmaps())      // odczytanie bitmap
      {
        CleanUp();            // zwolnienie pamięci 
        printf("\nError loading bitmaps\n");
        exit(1);
      }
    }

//
//  Wykonanie operacji "porządkowych" 
//
    void CleanUp()
    {
      RestoreVideo();       // odtworzenie trybu video
      RestoreKeyboard();    // odtworzenie obsługi klawiatury
      FreeMem();            // zwolnienie pamięci
    }

//
// To jest początek głównego programu. Ta funkcja wywołuje funkcje inicjalizacyjną.
// Następnie odczytuje zegar i uruchamia pętle animacyjną i na koniec ponownie odczytuje zegar
// Wartości zegara są wykorzystywane do obliczenia prędkości. 
//
    int main()
    {
      clock_t begin,fini;

      Initialize();          // ustawienie trybu video, ładowanie bitmap. itp.

      begin=clock();         // odczytanie zegara
      AnimLoop();            // wykonianie animacji
      fini=clock();          // ponowne odczytanie zegara

      CleanUp();             // zwolnienie pamięci, operacje "porządkowe" 
      printf("Frames: %d\nfps: %f\n",frames,(float)CLK_TCK*frames/(fini-begin));
      return 0;
    }

A to nagłowek :

//
// Ten nagłówek definiuje stałe i struktury używane w programie
//
    #define KEYBOARD 0x09
//
// Kody naciśnięcia i zwolnienia klawiszy wykorzystywane przez procedure obsługi przerwania INT 9h
//
    #define RIGHT_ARROW_PRESSED   77
    #define RIGHT_ARROW_REL      205
    #define LEFT_ARROW_PRESSED    75
    #define LEFT_ARROW_REL       203
    #define ESC_PRESSED          129
    #define UP_ARROW_PRESSED      72
    #define UP_ARROW_REL         200
    #define DOWN_ARROW_PRESSED    80
    #define DOWN_ARROW_REL       208

    #define VIEW_WIDTH    320
    #define VIEW_HEIGHT   150
    #define MEMBLK        VIEW_WIDTH*VIEW_HEIGHT
    #define TRANSPARENT   0       // indeks koloru przezroczystych pikseli
    #define TOTAL_SCROLL  320

    enum {NORMAL,RLE};
    enum {FALSE,TRUE};

    typedef struct
    {
      char manufacturer;    /* Zawsze ma wartość 0 */
      char version;         /* Zawsze 5, jeśli paleta kolorów wynosi 256 */
      char encoding;        /* Zawsze ma wartość 1 */
      char bits_per_pixel;  /* Powinno mieć 8 w przypadku plików 256 kolorowych */
      int  xmin,ymin;       /* Wspołrzędne górnego lewego rogu */
      int  xmax,ymax;       /* Szerokość i wysokość obrazu */
      int  hres;            /* Pozioma rozdzielczość obrazu */
      int  vres;            /* Pionowa rozdzielczość obrazu */
      char palette16[48];   /* Paleta karty EGA; nie używana w przypadku obrazów 256 kolorów */
      char reserved;        /* Zarezerwowane w przypadku przyszłych zastosowań */
      char color_planes;    /*  */
      int  bytes_per_line;  /* Liczba bajtów w jednym wierzszu */
      int  palette_type;    /* Ma wartość 2, w przypadku palety kolorowej */
      char filler[58];      /* */
    } PcxHeader;

    typedef struct
    {
      PcxHeader hdr;
      char *bitmap;
      char pal[768];
      unsigned imagebytes,width,height;
    } PcxFile;

    #define PCX_MAX_SIZE 64000L
    enum {PCX_OK,PCX_NOMEM,PCX_TOOBIG,PCX_NOFILE};

#ifdef __cplusplus
extern "C" {
#endif

    int ReadPcxFile(char *filename,PcxFile *pcx);
    void _interrupt NewInt9(void);
    void RestoreKeyboard(void);
    void InitKeyboard(void);
    void SetAllRgbPalette(char *pal);
    void InitVideo(void);
    void RestoreVideo(void);
    int InitBitmaps(void);
    void FreeMem(void);
    void DrawLayers(void);
    void AnimLoop(void);
    void Initialize(void);
    void CleanUp(void);
    void OpaqueBlt(char *,int,int,int);
    void TransparentBlt(char *,int,int,int);

#ifdef __cplusplus
  }
#endif

Literatura :
Sztuczki i Tajemnice Programowania Gier, LaMothe, Ratcliff, Seminatore & Tyler, LT&P, Warszawa 1996
Podręczk użytkownika systemu MS-DOS 6.22, Wrocław 1996

5 komentarzy

asm i C, ok, to sie teraz przydaje tylko do programowania hardware'u, ale nie do zastosowań dla jakich kolwiek End-Userów. Biblioteki graficzne + asm dla MMX i nowszych instrukcji -to jest to! Diabelnie szybkie i uzywane (np. blending). Ale 13h to juz zdecydowanie przezytek, mile wspomniania i tyle :)

hmm pomysl dobry ale stary, wszystko wporzo ale ta implementacja nie bedzie zdatna w zyciu dlatego ze jest zawolna. Jezeli chcesz miec plynna animacje uzywaj tego samego pomyslu ale w oparciu o pamiec karty VGA w trybie modeX...

ja tam korzystam raczej z asm'a i C czystego :P

eh, kto w tym jeszcze pisze ;P
teraz się korzysta z OpenGL'a :]

aby dzialalo nalezy zmienic nazwe pliku z LAYERS.PCX na BACKGRND.PCX wtedy dziala ale tez chyba cos nie do konca (przynajmniej u mnie)