La grafica 2D del Nintendo DS


Gli sprite

Ognuno dei due motori grafici del Nintendo DS integra dell'hardware dedicato alla visualizzazione degli sprite. E' piuttosto potente e flessibile e permette di gestire fino a 128 sprite contemporaneamente, ognuno a 16 o 256 colori (o anche a 32768 colori, attraverso i bitmap objects), e possono essere quadrati o rettangolari di varie dimensioni partendo dal più piccolo possibile, 8x8 pixel, fino al più grande, il 64x64 pixel. Il motore grafico è in grado anche di disegnare lo sprite ribaltato orizzontalmente (mirror) e/o verticalmente (flip) e gli sprite dichiarati di un certo tipo possono anche essere ruotati e/o ridimensionati. Oltre a questo si possono anche dichiarare sprite semitrasparenti e anche attivare un effetto mosaico su di essi.

In modo simile a come vengono archiviate in memoria le tessere per i background di tipo testo, anche per gli sprite bisognerà memorizzare dei blocchetti di 8x8 pixel che poi verranno visualizzati affiancati per definire ogni singolo sprite: per questo le dimensioni possibili per gli sprite sono tutte multiple di 8x8 pixel. Ecco tutte le possibilità e come ottenerle attraverso le costanti definite in libnds (in sprite.h), che saranno necessarie per impostare gli attributi di taglia e di forma dello sprite:

Dimensione (pixel, HxV) Taglia (14° e 15° bit dell'attributo 1) Forma (14° e 15° bit dell'attributo 0)
8x8 ATTR1_SIZE_8 ATTR0_SQUARE
16x8 ATTR1_SIZE_8 ATTR0_WIDE
8x16 ATTR1_SIZE_8 ATTR0_TALL
16x16 ATTR1_SIZE_16 ATTR0_SQUARE
32x8 ATTR1_SIZE_16 ATTR0_WIDE
8x32 ATTR1_SIZE_16 ATTR0_TALL
32x32 ATTR1_SIZE_32 ATTR0_SQUARE
32x16 ATTR1_SIZE_32 ATTR0_WIDE
16x32 ATTR1_SIZE_32 ATTR0_TALL
64x64 ATTR1_SIZE_64 ATTR0_SQUARE
64x32 ATTR1_SIZE_64 ATTR0_WIDE
32x64 ATTR1_SIZE_64 ATTR0_TALL

Prima di entrare nel vivo della dichiarazione degli sprite, è necessario chiarire come i blocchetti di 8x8 pixel devono essere memorizzati nei banchi di memoria video per poter poi essere correttamente utilizzati. A questo riguardo noi tratteremo solo una delle due modalità possibili, che niente hanno a che vedere con lo sprite in sé bensì solo con il sistema di memorizzazione dei blocchetti in memoria: la modalità 1D. Questo perché l'altra modalità (chiamata 2D) apporta pochissimi vantaggi (può apparire più semplice) ma introduce grandi svantaggi, il principale è che limita la memoria utilizzabile a 32 KB al massimo per ciascuno dei due motori grafici, mentre con la modalità 1D sarà possibile utilizzare fino a 256 KB di memoria video per i nostri sprite sul motore principale e fino a 128 KB sul motore secondario.

Nella modalità 1D ognuna delle tessere di 8x8 pixel appartenenti al medesimo sprite deve essere contigua alla precedente e la prima di queste tessere deve essere posizionata in memoria in una locazione allineata ad un certo limite che cambia a seconda di quanta memoria vorremo assegnare agli sprite. Lo chiariremo meglio più avanti, per ora affrontiamo il problema della contiguità delle tessere.

Come abbiamo detto ogni sprite è definito come una o più tessere di 8x8 pixel affiancate e/o sovrapposte. Perché lo sprite sia definito correttamente è obbligatorio memorizzare le tessere in memoria partendo da quella in alto a sinistra e procedendo a destra per poi andare a capo nella eventuale riga successiva, ovvero come faremmo leggendo un testo.

Ecco due esempi: dentro ogni quadratino il numero indica quale numero avrà la tessera in memoria.

sprite 32x16 all'inizio della memoria

 0  1  2  3
 4  5  6  7

 

sprite 8x32 memorizzato a partire dalla tessera 64

64
65
66
67

Il numero della tessera che si trova più in alto e più a sinistra sarà il numero a cui faremo riferimento quando chiederemo al nostro Nintendo DS di visualizzare lo sprite. Il numero massimo possibile è solo 1023 e proprio per questo motivo, ovvero per poter avere in memoria più di 1024 tessere, nella modalità 1D (e, appunto, solo in questa modalità) si usa un trucco che consente di raddoppiare, quadruplicare o ottuplicare la memoria utilizzabile per le tessere componenti gli sprite semplicemente assegnando il successivo numero di tessera solo ad una tessera ogni 2, oppure solo ad una tessera ogni 4, o ancora solamente a una tessera ogni 8. Le tessere senza numero, quindi, non potranno contenere l'inizio di uno sprite ma solo i suoi blocchetti successivi. Ricordatevi anche che ogni tessera a 16 colori occupa solo 32 byte (ogni byte contiene 2 pixel) mentre ogni tessera a 256 colori occupa 64 byte (un byte per ogni pixel).

Oltre alla costante DISPLAY_SPR_ACTIVE, necessaria ad attivare gli sprite, e alla costante DISPLAY_SPR_1D, necessaria per informare il motore che gli sprite sono immagazzinati in memoria con modalità 1D, vengono definite in libnds (nel file video.h) le seguenti costanti che sono anche queste da utilizzare nella funzione videoSetMode() (di cui abbiamo già parlato) quando vogliamo attivare anche gli sprite, oltre alla modalità prescelta per i background:

Costante Significato
DISPLAY_SPR_1D_SIZE_32 Potremo usare fino a 32 KB per gli sprite.
Sarà assegnato un nuovo numero di tessera ogni 32 byte: quindi un nuovo numero per ogni tessera a 16 colori e 2 nuovi numeri per ogni tessera a 256 colori.
DISPLAY_SPR_1D_SIZE_64 Potremo usare fino a 64 KB per gli sprite.
Sarà assegnato un nuovo numero di tessera ogni 64 byte: quindi un nuovo numero ogni 2 tessere a 16 colori e un nuovo numero ogni tessera a 256 colori.
DISPLAY_SPR_1D_SIZE_128 Potremo usare fino a 128 KB per gli sprite.
Sarà assegnato un nuovo numero di tessera ogni 128 byte: quindi un nuovo numero ogni 4 tessere a 16 colori e un nuovo numero ogni 2 tessere a 256 colori.
DISPLAY_SPR_1D_SIZE_256 Potremo usare fino a 256 KB per gli sprite (solo per il MAIN engine).
Sarà assegnato un nuovo numero di tessera ogni 256 byte: quindi un nuovo numero ogni 8 tessere a 16 colori e un nuovo numero ogni 4 tessere a 256 colori.

Personalmente trovo questo sistema oltremodo complicato e preferisco ragionare in modo diverso. Ad esempio usando gli sprite a 256 colori e con 128 KB di memoria dedicati agli sprite preferisco pensare che vi siano 2048 possibili tessere invece di 1024 e definisco una macro da usare quando si dovrà referenziare la prima tessera dello sprite. Ad esempio qualcosa di questo tipo:

#define TILE256_1D_128K(n) ((n)>>1)

A questo punto mi basta ricordare, in fase di caricamento degli sprite in memoria, di iniziare a caricare ogni sprite in modo che la sua prima tessera occupi una locazione pari. Analogamente per 256 KB di memoria video calcolo di avere 4096 possibili tessere (sempre a 256 colori) e mi definisco la seguente macro:

#define TILE256_1D_256K(n) ((n)>>2)

così quindi basta ricordare di iniziare a caricare ogni sprite in modo che la sua prima tessera occupi una locazione multipla di 4.

L'OAM

Ogni motore grafico del Nintendo DS ha un'area di memoria riservata dove memorizzare gli attributi di ogni oggetto (gli sprite): l'Object Attribute Memory, appunto OAM. Questa memoria è riservata alla definizione dei 3 attributi necessari per ogni singolo sprite; ogni attributo però non è una singola proprietà, bisogna pensare piuttosto ai 3 attributi insieme come ad un coacervo di tutte le proprietà dello sprite: il suo tipo, la sua taglia, la sua forma, la sua posizione sullo schermo, il suo numero di colori, la sua eventuale semitrasparenza e i suoi effetti come ad esempio l'effetto mosaico. Il tutto ammassato in 48 bit (16 bit per 3 attributi). Vediamo quindi come sono distribuite le proprietà sugli attributi e quali sono le costanti definite da libnds (in sprite.h) per ogni proprietà:

Attributo Proprietà
Attributo 0 Tipo:

ATTR0_NORMAL
ATTR0_ROTSCALE
ATTR0_ROTSCALE_DOUBLE
ATTR0_DISABLED

Subtipo:

ATTR0_TYPE_NORMAL
ATTR0_TYPE_BLENDED
ATTR0_TYPE_WINDOWED
ATTR0_BMP

Forma:

ATTR0_SQUARE
ATTR0_WIDE
ATTR0_TALL

Colori:

ATTR0_COLOR_16
ATTR0_COLOR_256

Effetto mosaico (flag):

ATTR0_MOSAIC

Posizione Y sullo schermo: (0-255)

OBJ_Y(m)

Attributo 1 Taglia:

ATTR1_SIZE_8
ATTR1_SIZE_16
ATTR1_SIZE_32
ATTR1_SIZE_64

Ribaltamenti:

ATTR1_FLIP_X
ATTR1_FLIP_Y

Indice nell'array dei dati di rotazione (solo per gli sprite rotazionali): (0-31)

ATTR1_ROTDATA(n)

Posizione X sullo schermo: (0-511)

OBJ_X(m)

Attributo 2 Priorità: (0-3)

ATTR2_PRIORITY(n)

Palette (solo con le palette estese attive): (0-15)

ATTR2_PALETTE(n)

Trasparenza (solo per i bitmap objects): (0-15)

ATTR2_ALPHA(n)

Tessera in memoria dove è memorizzato il primo blocchetto dello sprite: (0-1023)

(nessuna macro definita, conviene fare & 0x03ff per evitare sovrascritture degli altri bit)

Come si può vedere le proprietà sono davvero molte, entriamo nel dettaglio delle principali, parleremo delle altre quando ne avremo bisogno.

Nell'esempio che segue definiremo uno sprite di 16x16 pixel a 16 colori e lo sposteremo sullo schermo trascinando il pennino, in modo analogo a come abbiamo fatto in uno degli esempi dei background, ma questa volta senza attivare nessun background.

#include <nds.h>

// una comoda define...
#define TILE16_1D_128K(n) ((n)>>2)

int main(void) {

  // il nostro array di sprite (sull'OAM)
  OAMTable* Sprites = (OAMTable*)OAM;

  // azzeriamo l'OAM (disabilitiamo tutti gli sprite)
  int i;
  for (i=0;i<128;i++)
    Sprites->oamBuffer[i].attribute[0]=ATTR0_DISABLED | OBJ_Y(192);
    
  // impostiamo la memoria video (banco A) per gli sprite
  vramSetBankA (VRAM_A_MAIN_SPRITE);
  
  // impostiamo il modo 0 sul MAIN engine e attiviamo gli sprite (modo 1D, 128KB RAM)
  videoSetMode (MODE_0_2D|DISPLAY_SPR_ACTIVE|DISPLAY_SPR_1D|DISPLAY_SPR_1D_SIZE_128);
  
  // creiamo lo sprite 16x16, 4 bpp (32 bytes x 4 tessere)
  u32 Sprite[8*4] = 
  {
    0x10000000,
    0x21000000,
    0x22100000,
    0x22210000,
    0x00001000,
    0x00000100,
    0x00000010,
    0x00000001,
  
    0x00000001,
    0x00000012,
    0x00000122,
    0x00001222,
    0x00010000,
    0x00100000,
    0x01000000,
    0x10000000,
    
    0x00000001,
    0x00000010,
    0x00000100,
    0x00001000,
    0x22210000,
    0x22100000,
    0x21000000,
    0x10000000,
    
    0x10000000,
    0x01000000,
    0x00100000,
    0x00010000,
    0x00001222,
    0x00000122,
    0x00000012,
    0x00000001
  };
  
  // copiamo le tessere in memoria video (32 bytes ognuna)
  swiCopy(Sprite, SPRITE_GFX, 32*4);
  
  // impostiamo la palette ai colori 0=nero, 1=blu intenso, 2=bianco
  SPRITE_PALETTE [0] = RGB5(0,0,0);
  SPRITE_PALETTE [1] = RGB5(0,0,31);
  SPRITE_PALETTE [2] = RGB5(31,31,31);
  
  // associamo il MAIN engine allo schermo inferiore, il touchscreen
  lcdMainOnBottom ();
  
  int down_X=0, down_Y=0;
  while(1) {
  
    // leggiamo lo stato dei tasti e del touch screen
    scanKeys();
    
    // abbiamo appena toccato il touch screen?
    if (keysDown() & KEY_TOUCH) {
     
      // leggi la posizione del pennino
      touchPosition touch; touchRead(&touch);
      
      // memorizziamola per vedere dove va a finire
      down_X=touch.px;
      down_Y=touch.py;
    }
  
    // oppure siamo ancora appoggiati sul touch screen?
    else if (keysHeld() & KEY_TOUCH) {
     
      // leggi la posizione del pennino
      touchPosition touch; touchRead(&touch);
      
      // sposta lo sprite 0 seguendo il trascinamento
      Sprites->oamBuffer[0].attribute[0]=ATTR0_NORMAL|ATTR0_TYPE_NORMAL|ATTR0_COLOR_16
                                        |ATTR0_SQUARE|OBJ_Y((192-16)/2-down_Y+touch.py);
      Sprites->oamBuffer[0].attribute[1]=ATTR1_SIZE_16|OBJ_X((256-16)/2-down_X+touch.px);
      Sprites->oamBuffer[0].attribute[2]=ATTR2_PRIORITY(0)|(TILE16_1D_128K(0) & 0x03ff);
    }
    
    // altrimenti riallinea lo sprite al centro
    else {
     
      // sposta lo sprite 0 al centro
      Sprites->oamBuffer[0].attribute[0]=ATTR0_NORMAL|ATTR0_TYPE_NORMAL|ATTR0_COLOR_16
                                        |ATTR0_SQUARE|OBJ_Y((192-16)/2);
      Sprites->oamBuffer[0].attribute[1]=ATTR1_SIZE_16|OBJ_X((256-16)/2);
      Sprites->oamBuffer[0].attribute[2]=ATTR2_PRIORITY(0)|(TILE16_1D_128K(0) & 0x03ff);
    }
    
    // aspettiamo il prossimo refresh
    swiWaitForVBlank();
  }
}

Nell'esempio si nota subito che abbiamo dovuto disattivare tutti gli sprite prima di iniziare: se non avessimo fatto questo ci saremmo trovati con tutti gli sprite accatastati nell'angolo in alto a sinistra dello schermo. Invece attraverso la proprietà ATTR0_DISABLED segnaliamo che lo sprite non è da disegnare. Per disattivare uno sprite, oltre a impostare questa modalità, molte fonti consigliano anche di impostare la coordinata Y al valore 192 così da alleggerire il carico di lavoro al motore degli sprite che sembrerebbe prima confrontare questo valore con la scanline (riga di pixel dello schermo) che sta disegnando in quel momento e solo dopo controllare se lo sprite è attivo o meno. Un'altra cosa molto importante che si nota dall'esempio è che libnds definisce un tipo OAMTable, un array di 128 strutture oamBuffer per accedere direttamente ad ogni singolo attributo di ogni singolo sprite, così che risulti molto semplice accedere all'OAM.

Prestando molta attenzione invece a come abbiamo definito le 4 tessere che compongono il nostro sprite, forse vi sarete accorti che sembrano ribaltate orizzontalmente. Invece non è così ma, semplicemente, i pixel di ogni tessera vengono disegnati da sinistra verso destra (come ci si può aspettare) ma devono essere memorizzati nella memoria video dal meno significativo al più significativo, per questo 0x00000001 assegna il colore 1 al pixel più a sinistra (tra gli 8 pixel orizzontali per ciascuna riga di questa tessera a 16 colori) mentre per contro il valore 0x20000000 assegna il colore 2 al pixel più a destra.

In modo analogo a come capitava per i background poi, anche per gli sprite esiste una costante che punta all'inizio della memoria loro dedicata, indipendentemente dai banchi di memoria video assegnati a questa funzione: è SPRITE_GFX, per il MAIN engine, e SPRITE_GFX_SUB per il SUB engine. A questo indirizzo in memoria copiamo le nostre 4 tessere, così che il motore grafico le possa utilizzare.

Gli sprite rotazionali

Dentro a ogni OAM, oltre alle definizioni dei 3 attributi per ognuno dei 128 sprite, c'è ancora un po' di spazio dedicato alla definizione dei dati di rotazione/dimensionamento, ma non abbastanza per poter definire questi dati singolarmente per ogni sprite, e così chi ha progettato questo sistema ha deciso di creare 'solo' 32 definizioni e chiamarle classi, e per ognuna di queste classi sono necessari 4 registri a 16 bit, che sono di più di quelli che già usiamo normalmente per ogni singolo sprite. Questi 4 registri sono analoghi ai registri PA, PB, PC e PD che abbiamo visto nei background rotazionali, e funzionano quasi esattamente allo stesso modo. Con questo sistema quindi non potremo ruotare indipendentemente ogni sprite ma dovremo accontentarci di accorpare in classi gli sprite che vogliamo far ruotare e/o ridimensionare e accettare che tutti gli sprite della stessa classe ruotino e/o si ridimensionino nello stesso modo. Non è un problema molto limitante, come è facile intuire.

Nell'esempio che segue definiremo (sul SUB engine) uno sprite di 16x16 pixel a 256 colori e lo faremo ruotare su se stesso usando il pulsante A, e potremo azzerare il suo angolo con il pulsante B. Invece tenendo premuto uno dei due tasti sulla spalla del Nintendo DS potremo attivare la modalità rotazionale a dimensioni raddoppiate, per vedere la differenza.

#include <nds.h>

// un'altra comoda define...
#define TILE256_1D_128K(n) ((n)>>1)

// questa funzione genera un "falso" coseno, usando un'onda triangolare
// restituisce +1 .. -1 in virgola fissa 24.8
int fake_cos (int angle) {
  int ret=0;
  angle %= 360;
  if (angle<0)
    angle += 360;

  switch (angle) {
    case 0 ... 180:ret = 256-(512*angle/180);
                    break;
    case 181 ... 359:ret = ((angle-180)*512/180) - 256;
                      break;
  }
  return (ret);
}

// sin(x) = cos(90-x)
int fake_sin (int angle) {
  return (fake_cos(90 - angle));
}

int main(void) {

  // il nostro array di sprite (sull'OAM_SUB)
  OAMTable* Sprites = (OAMTable*)OAM_SUB;

  // azzeriamo l'OAM (disabilitiamo tutti gli sprite)
  int i;
  for (i=0;i<128;i++)
    Sprites->oamBuffer[i].attribute[0]=ATTR0_DISABLED | OBJ_Y(192);
    
  // impostiamo la memoria video (banco D) per gli sprite
  vramSetBankD (VRAM_D_SUB_SPRITE);
  
  // impostiamo il modo 0 sul SUB engine e attiviamo gli sprite (modo 1D, 128KB RAM)
  videoSetModeSub (MODE_0_2D|DISPLAY_SPR_ACTIVE|DISPLAY_SPR_1D|DISPLAY_SPR_1D_SIZE_128);
  
  // creiamo lo sprite 16x16, 256 colori (64 bytes x 4 tessere)
  u8 Sprite[64*4] = 
  {
    2,2,2,2,2,2,2,2,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    
    2,2,2,2,2,2,2,2,
    3,3,3,3,3,3,3,2,
    1,3,3,3,3,3,3,2,
    1,3,3,3,3,3,3,2,
    1,3,3,3,3,3,3,2,
    1,3,3,3,3,3,3,2,
    1,3,3,3,3,3,3,2,
    1,3,3,3,3,3,3,2,

    2,3,3,3,3,3,3,1,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,3,3,3,3,3,3,3,
    2,2,2,2,2,2,2,2,

    1,1,3,3,3,3,3,2,
    1,3,3,3,3,3,3,2,
    3,3,3,3,3,3,3,2,
    3,3,3,3,3,3,3,2,
    3,3,3,3,3,3,3,2,
    3,3,3,3,3,3,3,2,
    3,3,3,3,3,3,3,2,
    2,2,2,2,2,2,2,2
  };
  
  // copiamo le tessere in memoria video (32 bytes ognuna)
  swiCopy(Sprite, SPRITE_GFX_SUB, 64*4);

  // impostiamo la palette ai colori 1=rosso, 2=grigio, 3=blu,
  SPRITE_PALETTE_SUB [1] = RGB5(31,0,0);
  SPRITE_PALETTE_SUB [2] = RGB5(15,15,15);
  SPRITE_PALETTE_SUB [3] = RGB5(0,0,15);
  
  // associamo il SUB engine allo schermo superiore
  lcdMainOnBottom ();
  
  // 'azzera' la classe di rotazione 0
  Sprites->matrixBuffer[0].hdx = 1 << 8;
  Sprites->matrixBuffer[0].hdy = 0;
  Sprites->matrixBuffer[0].vdx = 0;
  Sprites->matrixBuffer[0].vdy = 1 << 8;
  
  // disegna lo sprite 0 al centro dello schermo
  Sprites->oamBuffer[0].attribute[0]=ATTR0_ROTSCALE|ATTR0_TYPE_NORMAL
                                    |ATTR0_COLOR_256|ATTR0_SQUARE|OBJ_Y((192-16)/2);
  Sprites->oamBuffer[0].attribute[1]=ATTR1_SIZE_16|ATTR1_ROTDATA(0)|OBJ_X((256-16)/2);
  Sprites->oamBuffer[0].attribute[2]=ATTR2_PRIORITY(0)|(TILE256_1D_128K(0) & 0x03ff);

  int angle=0;
  while(1) {
  
    // leggiamo lo stato dei tasti e del touch screen
    scanKeys();
  
    // stiamo premendo A ?
    if (keysHeld() & KEY_A)
     
      // cambiamo angolo
      angle=(angle+15) % 360;

    
    // abbiamo premuto B ?
    if (keysDown() & KEY_B)
     
      // azzeriamo l'angolo
      angle=0;

    // ruota lo sprite 0 modificando la classe 0 a seconda del frame
    Sprites->matrixBuffer[0].hdx = fake_cos (angle);
    Sprites->matrixBuffer[0].hdy = -fake_sin (angle);
    Sprites->matrixBuffer[0].vdx = fake_sin (angle);
    Sprites->matrixBuffer[0].vdy = fake_cos (angle);

    // stiamo premendo uno dei tasti sulla spalla del DS?
    if (keysHeld() & (KEY_L|KEY_R)) {
     
      // premuto: attiva la modalità rotazionale 'a dimensione doppia'
      Sprites->oamBuffer[0].attribute[0]=ATTR0_ROTSCALE_DOUBLE|ATTR0_TYPE_NORMAL
                                        |ATTR0_COLOR_256|ATTR0_SQUARE|OBJ_Y((192-32)/2);
      Sprites->oamBuffer[0].attribute[1]=ATTR1_SIZE_16|ATTR1_ROTDATA(0)|OBJ_X((256-32)/2);
    } else {
     
      // non premuto: attiva la modalità rotazionale 'normale'
      Sprites->oamBuffer[0].attribute[0]=ATTR0_ROTSCALE|ATTR0_TYPE_NORMAL
                                        |ATTR0_COLOR_256|ATTR0_SQUARE|OBJ_Y((192-16)/2);
      Sprites->oamBuffer[0].attribute[1]=ATTR1_SIZE_16|ATTR1_ROTDATA(0)|OBJ_X((256-16)/2);
    }

    // aspettiamo il prossimo refresh
    swiWaitForVBlank();
  }
}

La prima cosa che si nota è che libnds definisce all'interno del tipo OAMTable, oltre all'array di 128 strutture oamBuffer, un secondo array di 32 strutture matrixBuffer per accedere ad ognuno dei 4 registri di ogni singola classe di rotazione. I 4 registri a 16 bit hanno nomi hdx, hdy, vdx e vdy ma hanno esattamente lo stesso significato e lo stesso uso che hanno i 4 registri PA, PB, PC e PD per i background.

Un altro dettaglio notevole è che in questo caso le tessere che compongono lo sprite sono a 256 colori e, dichiarandole come array di byte, questi valori si accodano esattamente nello stesso ordine di come poi i pixel verranno disegnati dentro lo sprite.

Infine, molto importante, provando il programma sul Nintendo DS o su un emulatore si può facilmente capire come funziona la modalità rotazionale a dimensioni raddoppiate: lo sprite di per sé non cambia di dimensioni (per ottenere questo effetto possiamo agire sulla matrice) ma il suo contenitore, per così dire, diventa più capiente e quindi pur ruotando lo sprite all'interno del suo rettangolo, essendo questo raddoppiato non taglieremo via una parte del contenuto. A meno di non ruotare degli sprite 8x32, ad esempio: in questo caso ovviamente una delle dimensioni dello sprite è comunque troppo grande per poter stare all'interno dell'altra dimensione raddoppiata.

I bitmap objects

Oltre agli sfondi a 32768 colori, che abbiamo già visto, il Nintendo DS consente anche di definire sprite a 15 bpp più un bit di alfa per indicare quali pixel dovranno essere disegnati e quali no, in modo analogo ai background di tipo bitmap. Questi oggetti bitmap, come vengono chiamati, possono essere memorizzati nella memoria video in modalità 1D o 2D in modo analogo agli sprite, ma non in modo del tutto identico, e quindi sono definite in libnds (nel file video.h) le seguenti costanti (per la modalità di memorizzazione 1D, come al solito tralasceremo la modalità 2D) che dovranno essere utilizzate insieme alla costante DISPLAY_SPR_1D_BMP che informerà il motore grafico riguardo le nostre intenzioni:

Costante Significato
DISPLAY_SPR_1D_BMP_SIZE_128 Potremo usare fino a 128 KB per i bitmap objects.
Sarà assegnato un nuovo numero di tessera ogni 128 byte: quindi un nuovo numero ogni tessera a 32768 colori.
DISPLAY_SPR_1D_BMP_SIZE_256 Potremo usare fino a 256 KB per gli i bitmap objects (solo per il MAIN engine).
Sarà assegnato un nuovo numero di tessera ogni 256 byte: quindi un nuovo numero ogni 2 tessere a 32768 colori.

Nell'esempio molto semplice che segue (non c'è neanche interattività) disegneremo due bitmap objects sovrapposti.

#include <nds.h>

int main(void) {

  // il nostro array di sprite (sull'OAM)
  OAMTable* Sprites = (OAMTable*)OAM;

  // azzeriamo l'OAM (disabilitiamo tutti gli sprite)
  int i;
  for (i=0;i<128;i++)
    Sprites->oamBuffer[i].attribute[0]=ATTR0_DISABLED | OBJ_Y(192);
    
  // impostiamo la memoria video (banco A) per gli sprite
  vramSetBankA (VRAM_A_MAIN_SPRITE);
  
  // impostiamo il modo 0 sul MAIN engine e attiviamo gli sprite (modo BMP 1D, 128KB RAM)
  videoSetMode (MODE_0_2D|DISPLAY_SPR_ACTIVE
                |DISPLAY_SPR_1D_BMP|DISPLAY_SPR_1D_BMP_SIZE_128);
  
  // creiamo uno sprite 8x8, una X bianca a 32768 colori (128 bytes)
  u16 SpriteC[8*8] = 
  {
    0xffff,0x0101,0x0000,0x0000,0x0000,0x0000,0x0000,0xffff,
    0x0000,0xffff,0x0000,0x0101,0x0101,0x0000,0xffff,0x0000,
    0x0000,0x0000,0xffff,0x0000,0x0000,0xffff,0x0000,0x0000,
    0x0101,0x0101,0x0000,0xffff,0xffff,0x0000,0x0101,0x0101,
    0x0101,0x0101,0x0000,0xffff,0xffff,0x0000,0x0101,0x0101,
    0x0000,0x0000,0xffff,0x0000,0x0000,0xffff,0x0000,0x0000,
    0x0000,0xffff,0x0000,0x0101,0x0101,0x0000,0xffff,0x0000,
    0xffff,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0xffff
  };
  
  // creiamo uno sprite 16x16, 32768 colori, vuoto (128 byte x 4)
  u16 SpriteS[8*8*4];
  
  // riempiamo le 4 tessere dello sprite 16x16 di sfumature di colore
  int t,j,k;
  for (t=0;t<4;t++)
    for (k=0;k<8;k++)
      for (j=0;j<8;j++)
        SpriteS[t*64+k*8+j]=RGB5(k*4,j*4,t*8) | BIT (15);

  // copiamo tutte le tessere in memoria video
  swiCopy(SpriteC, SPRITE_GFX, 128);
  swiCopy(SpriteS, SPRITE_GFX + 128, 128*4);
  
  // associamo il MAIN engine allo schermo inferiore, il touchscreen
  lcdMainOnBottom ();
  
  // disegnamo lo sprite 0 usando la X bianca al centro dello schermo, davanti
  Sprites->oamBuffer[0].attribute[0]=ATTR0_BMP|ATTR0_SQUARE|OBJ_Y((192-8)/2);
  Sprites->oamBuffer[0].attribute[1]=ATTR1_SIZE_8|OBJ_X((256-8)/2);
  Sprites->oamBuffer[0].attribute[2]=ATTR2_PRIORITY(0)|(0 & 0x03ff);

  // disegnamo lo sprite 1 usando la sfumatura al centro dello schermo, dietro
  Sprites->oamBuffer[1].attribute[0]=ATTR0_BMP|ATTR0_SQUARE|OBJ_Y((192-16)/2);
  Sprites->oamBuffer[1].attribute[1]=ATTR1_SIZE_16|OBJ_X((256-16)/2);
  Sprites->oamBuffer[1].attribute[2]=ATTR2_PRIORITY(0)|(1 & 0x03ff);  
  
  while(1) {
  
    // aspettiamo il prossimo refresh
    swiWaitForVBlank();
  }
}

Nell'esempio ho volutamente aggiunto dei pixel colorati (ma senza alfa bit) allo sprite della X bianca: non sono stati disegnati, come potete notare eseguendo il programma.

Copia dell'OAM attraverso il DMA

Spesso può essere conveniente preparare in anticipo gli sprite che devono essere visualizzati nel prossimo fotogramma: questo però è impossibile agendo direttamente sull'OAM poiché si andrebbero a modificare gli sprite che si stanno disegnando nel fotogramma corrente; è quindi prassi comune riservare uno spazio in memoria esattamente strutturato come l'OAM e riempire questo spazio con le proprietà che vorremo dare agli sprite al prossimo fotogramma. Al momento giusto, poi, si tratterà di copiare tutta questa memoria (la sua dimensione è 1 KB) sull'OAM vero, ottenendo quindi l'effetto richiesto.

Nel Nintendo DS c'è anche dell'hardware dedicato ad effettuare copie di aree di memoria in altre aree di memoria senza occupare il processore per un'operazione così banale, e questo hardware si chiama DMA, Direct Memory Access. Per esempio potremo usare il DMA proprio per aggiornare l'OAM con il contenuto della nostra copia in memoria, e infatti è prassi piuttosto comune. C'è però un effetto collaterale che spesso viene dimenticato: dato che il processore principale del Nintendo DS è dotato di cache (una memoria interna al processore che serve, per esempio, a ritardare le operazioni di scrittura in memoria al fine di migliorare le performance nell'accesso a questa) può accadere che il DMA si metta a copiare informazioni non ancora aggiornate poiché ancora in cache. Questo problema inoltre non si presenta durante la fase di test sugli emulatori poiché questi ultimi non emulano il funzionamento della cache e quindi facilmente ci si ritrova con un programma funzionante sull'emulatore ma malfunzionante quando lo si prova su una consolle vera.

Per ovviare a questo problema c'è la possibilità di non utilizzare il DMA per effettuare la copia, e quindi si copierà questa memoria in modo convenzionale, oppure si dovrà imporre alla cache dei dati (intera o solo una parte di essa) di effettuare le scritture ancora sospese: un'operazione chiamata flush, scarico.

Nell'esempio che segue utilizzeremo tutti i 128 sprite del MAIN engine per disegnare delle stelline a caso sullo schermo. Alla pressione del tasto A riposizioneremo tutte le stelline utilizzando una copia dell'OAM, il flush della cache dei dati, e copiando gli sprite sull'OAM utilizzando un DMA.

#include <nds.h>

// una comoda define...
#define TILE16_1D_128K(n) ((n)>>2)

// definiamo il nostro array di sprite (una copia)
OAMTable Sprites;

void RandomizeSprites (void) {

  int i;
  
  // ognuno dei 128 sprite
  for (i=0;i<128;i++) {
  
    // normale, 16 colori, quadrato
    Sprites.oamBuffer[i].attribute[0]=ATTR0_NORMAL | ATTR0_TYPE_NORMAL | ATTR0_COLOR_16 |
                                      ATTR0_SQUARE | OBJ_Y(rand()%(192-8));
    // 8x8 pixel di dimensione
    Sprites.oamBuffer[i].attribute[1]=ATTR1_SIZE_8 | OBJ_X(rand()%(256-8));
    
    // tessera 0, quella con la stellina
    Sprites.oamBuffer[i].attribute[2]=ATTR2_PRIORITY(0) | (TILE16_1D_128K(0) & 0x03ff);
  }
}

int main(void) {

  // azzeriamo l'OAM (disabilitiamo tutti gli sprite)
  int i;
  for (i=0;i<128;i++)
    Sprites.oamBuffer[i].attribute[0]=ATTR0_DISABLED | OBJ_Y(192);
    
  // impostiamo la memoria video (banco A) per gli sprite
  vramSetBankA (VRAM_A_MAIN_SPRITE);
  
  // impostiamo il modo 0 sul MAIN engine e attiviamo gli sprite (modo 1D, 128KB RAM)
  videoSetMode (MODE_0_2D|DISPLAY_SPR_ACTIVE|DISPLAY_SPR_1D|DISPLAY_SPR_1D_SIZE_128);
  
  // creiamo lo sprite 8x8, 16 colori
  u32 Sprite[8] = 
  {
    0x00010000,
    0x00010000,
    0x00010000,
    0x11111110,
    0x00101000,
    0x01000100,
    0x10000010,
    0x00000000
  };
  
  // copiamo le tessere in memoria video (32 byte)
  swiCopy(Sprite, SPRITE_GFX, 32);
  
  // impostiamo la palette ai colori 0=nero, 1=bianco
  SPRITE_PALETTE [0] = RGB5(0,0,0);
  SPRITE_PALETTE [1] = RGB5(31,31,31);
  
  // associamo il MAIN engine allo schermo superiore
  lcdMainOnTop ();
  
  // posiziona gli sprite a caso
  RandomizeSprites();
  
  // setto il flag per indicare che l'OAM è da aggiornare
  int OAMRefresh=1;
  
  while(1) {
  
    // leggiamo lo stato dei tasti e del touch screen
    scanKeys();
    
    // abbiamo premuto A?
    if (keysDown() & KEY_A) {
     
      // posiziona gli sprite a caso
      RandomizeSprites();
      
      // setto il flag per indicare che l'OAM è da aggiornare
      OAMRefresh=1;
    }
  
    // aspettiamo il prossimo refresh
    swiWaitForVBlank();
    
    // la nostra copia dell'OAM è cambiata?
    if (OAMRefresh) {
     
      // flush della cache dei dati
      DC_FlushAll();
      
      // copia dell'OAM via DMA
      dmaCopy (&Sprites, OAM, sizeof(Sprites));
      
      // resetto il flag per indicare che l'OAM è aggiornato
      OAMRefresh=0;
    }
  }
}

Effetto Mosaico

L'ultima proprietà di cui tratto in questa sezione è il flag per attivare l'effetto mosaico, contenuto nell'attributo 0 di ogni sprite. Personalmente trovo discutibile l'utilità di questo effetto, però più di una volta mi è capitato di vederlo usare negli homebrew e quindi a qualcuno magari può interessare. Il mosaico è definito come il disegnare un certo numero di volte in orizzontale o verticale lo stesso pixel, invece dei pixel che dovrebbero essere veramente disegnati. Ad esempio un mosaico di dimensione 2x2 indica che il primo pixel dello sprite sarà ripetuto per due volte in orizzontale ed ugualmente nella riga inferiore, quindi nascondendo il colore originale del pixel immediatamente a destra, quello sotto, e quello a destra di quello sotto. Che sarebbe a dire come scendere di risoluzione senza diminuire la dimensione dello sprite, esattamente quello che si chiama mosaico, appunto, perché composto di rettangolini. Il flag nell'attributo 0 indica quindi se l'effetto deve essere applicato allo specifico sprite, ma per impostare quanti pixel è grande ogni tessera del mosaico bisogna utilizzare il registro apposito, definito in libnds (in video.h) come MOSAIC_CR per il MAIN engine e SUB_MOSAIC_CR per il SUB engine. E' un registro a 16 bit ed è diviso in 4 sezioni da 4 bit ognuna. Andando per ordine, i 4 bit meno significativi sono la dimensione orizzontale della tessera per l'effetto mosaico dei background (sì, anche i background possono avere questo effetto, agendo sui registri di controllo dei background attraverso la costante BG_MOSAIC_ON definita in background.h), poi la dimensione verticale, poi analogamente, nel byte più significativo, 4 bit per la dimensione orizzontale della tessera per l'effetto mosaico degli sprite e infine i 4 bit per la dimensione verticale. Ovviamente il valore 0 non avrebbe senso e quindi in realtà ogni valore andrà indicato abbassato di una unità, cioè per impostare una modalità mosaico di 8x6 pixel per gli sprite bisognerà associare al registro il valore 0x5700. Ovviamente il valore impostato qui sarà utilizzato indistintamente per tutti gli sprite con il flag di mosaico attivo.


Sverx, 19 Giugno 2009. Ultima modifica il 26 Giugno 2009.


Domande? Dubbi? Suggerimenti? Vuoi scambiare due parole con me? Ho preparato un forum apposta!

Torna all'indice per accedere alle altre sezioni.

riferimenti:

http://nocash.emubase.de/gbatek.htm#lcdobjoamattributes

http://nocash.emubase.de/gbatek.htm#dsvideoobjs

http://nocash.emubase.de/gbatek.htm#lcdobjoamrotationscalingparameters

http://nocash.emubase.de/gbatek.htm#dsdmatransfers

http://nocash.emubase.de/gbatek.htm#lcdiomosaicfunction