Uno sguardo dentro al Nintendo DS: la grafica 3D

Concludiamo questa breve serie sul Nintendo DS analizzando quella parte del Nitro Processor che si occupa di gestire la grafica poligonale: il 3D Graphics Engine.

Il 3D Graphics Engine ha un’architettura abbastanza semplice. Si compone sostanzialmente di due macro blocchi: il Geometry Engine (GE), che si occupa di processare i poligoni, ed il Rendering Engine (RE), che disegna i pixel a partire dalle informazioni processate dal Geometry Engine.

Non sono presenti unità programmabili, ma tutte le funzioni sono organizzate secondo una struttura, più o meno classica, a pipeline fissa.

Si parte da un buffer FIFO (First In First Out), che accoglie i comandi di rendering e li somministra agli stadi successivi, nello stesso ordine in cui arrivano.

Il Geometry Engine si occupa di 3 processi:

  1. Trasformazione dei vertici da un sistema di coordinate ad un altro
  2. Calcolo delle luci per ogni vertice
  3. Trasformazione delle coordinate delle texture

Il processo 1) consiste in una serie di moltiplicazioni tra vettori e matrici, dove i primi sono i vertici dei poligoni ed le seconde sono i coefficienti di un sistema di equazioni, dette funzioni di proiezione o trasformazioni affini. Sono i calcoli matematici che stanno alla base delle regole della prospettiva.

Il processo 2) non è molto diverso dal precedente, e in sostanza si considerano i vettori che rappresentano la direzione delle fonti luminose in relazione ai singoli vertici. Attraverso opportune funzioni si determinano l’intensità ed il colore della luce su un dato vertice. Per effettuare questi calcoli occorrono sia le coordinate dei vertici, sia le cosiddette normali. Le normali, o perpendicolari, sono semplici vettori perpendicolari alla superficie dell’oggetto. Giocando opportunamente con le normali si può modulare la luce in modo tale da sfumare gli spigoli degli oggetti poligonali, creando così delle ombreggiature uniformi, ma questo lo vedremo più in là, durante la fase di rendering.

Il processo 3) consiste anch’esso in una serie di trasformazioni affini, e serve per determinare quali pixel delle texture vanno mappati sulle coordinate dei vertici. Basta calcolare soltanto i pixel dei vertici, mentre quelli interni al poligono sono ricavati per interpolazione durante la fase successiva, dentro il Rendering Engine.

Si capisce a colpo d’occhio che questi processi sono molto simili tra loro, perchè in tutti i casi bisogna sostanzialmente eseguire somme e moltiplicazioni in gran quantità. Il Geometry Engine, per questo motivo, può essere considerato come un coprocessore matematico estremamente specializzato. Sa fare solo due operazioni (somma e prodotto), ma le esegue molto velocemente e su intere matrici in un solo colpo.

Per raggiungere questa efficienza mantenendo bassa la complessità dell’hardware, i progettisti del Nitro Processor hanno deciso di lavorare soltanto su numeri interi, perchè trattare i numeri in floating point sarebbe eccessivamente dispendioso.

Com’è possibile effettuare calcoli sulle coordinate usando soltanto numeri interi? Intuitivamente vien da pensare che con tutte queste moltiplicazioni tra matrici, qualche numero con la virgola ogni tanto ci scappa, no? Apriamo una breve parentesi per approfondire un po’ la questione.

La soluzione si chiama Fixed-Point Arithmetic, e consiste nel trattare i numeri interi come se fossero numeri con la virgola, tenendo invariata la posizione della virgola che viene stabilita a priori. Questo si contrappone alla Floating Point Arithmetic, dove il calcolo della posizione della virgola (l’esponente) comporta moltissimi calcoli aggiuntivi rispetto al calcolo del numero vero e proprio (la mantissa).

Nel nostro caso le coordinate, sia quelle dei vertici (x,y,z) che quelle delle texture (u,v), sono espresse usando numeri interi a 16 bit così ripartiti:

  • 1 bit per il segno
  • 3 bit per la parte intera (che può andare quindi da -7 a +7)
  • 12 bit per la parte decimale (da 0 fino a 4095 / 4096, che corrisponde a 0.999755859375)

Il massimo range rappresentabile varia quindi da -7.999755859375 a +7.999755859375, che possiamo tranquillamente approssimare a (-8.0, +8.0) estremi esclusi.

Nei calcoli interni al GE le coordinate nello spazio globale usano 16 bit per la parte intera, quindi si ha a disposizione uno spazio molto più ampio dove muoversi.

La matematica in Fixed Point ha il pregio di essere semplice da calcolare (richiede semplici ALU intere, al posto di complicate e costose FPU) e presenta una distribuzione perfettamente uniforme in tutto il range. Ciò significa che in genere i calcoli saranno più precisi, perchè soffre un po’ meno dei difetti di arrotondamento che affliggono i calcoli in virgola mobile. Lo svantaggio sta nel range molto limitato, inaccettabile per calcoli che richiedono la compresenza di numeri distanti tra loro di molti ordini di grandezza.

Nel nostro caso potremmo tranquillamente fare l’associazione 1 a 1 tra le unità del GE e i metri, e quindi possiamo tranquillamente accettare di avere un range di +/- 8.0 metri per descrivere un oggetto, e +/- 65.5 Km per descrivere il mondo all’interno del quale questo oggetto si muove. In base alle esigenze ovviamente si possono fare diverse associazioni, adottando un’opportuna scala, ma queste cose vanno concordate tra i programmatori ed i grafici che devono produrre i modelli 3D e le scene.

Chiusa la parentesi, proseguiamo con il Rendering Engine.

Una volta calcolate le coordinate che i poligoni avranno sullo schermo, si passa alla fase di Rastering, compiuta dal RE, che consiste nel calcolare il colore finale di ogni pixel dello schermo.

Nell’effettuare questo calcolo vengono prese in considerazione non solo il colore dei pixel della texture, ma anche il colore del vertice e quello della luce, ottenuto nel precedente passaggio da parte del Geometry Engine. Nell’equazione (o meglio nella catena di equazioni) entrano anche altri fattori quali il livello di trasparenza del poligono (o della texture), eventuali ombre proiettate da altri poligoni (stencil shadows) ed effetti vari (edge marking, antialiasing, toon shading, ecc…).

Il risultato del RE non viene ancora mostrato a schermo, ma subisce un ulteriore passaggio.

In base alla configurazione dei registri del Nitro Processor (ad opera del programmatore), l’output del RE può essere inviato al BG0 del 2D Graphics Engine A (che a sua volta effettuerà un ulteriore processo, fondendo il BG0 con il resto della grafica 2D), oppure può essere per così dire catturato in un banco di VRAM (una sorta di screenshot) per consentire eventuali effetti grafici di postprocessing (motion blur ad esempio) oppure per attuare alcune tecniche avanzate.

Una di queste tecniche, e forse la più utile e sfruttata ampiamente, consiste nell’effettuare uno screenshot ad ogni frame, poi scambiare il ruolo dello schermo superiore e quello inferiore, visualizzare lo screenshot precedente tramite il 2D Graphics Engine B (quello sprovviso di unità 3D), ed infine riscambiare gli schermi e procedere così ad libitum.

In questo modo si calcolano le scene 3D per entrambi gli schermi e tramite uno switch ad ogni fotogramma si possono apprezzare animazioni 3D su entrambi gli schermi ad un massimo di 30 fps (60 fps totali, alternando i frame pari sullo schermo superiore e i frame dispari su quello inferiore).

I dettagli di tutti i calcoli ed i passaggi effettuati dal GE e dal RE non sono mai entrati nel pubblico dominio, e quindi non li tratterò per evidenti motivi. Tuttavia il discorso merita di essere concluso con una breve rassegna dei numeri e delle caratteristiche tecniche di questa piccola GPU:

Formati di Texture

  • da 8×8 fino a 1024×1024, con dimensioni potenze di 2 (quindi 8, 16, 32, 64, 128, 256, 512 e 1024)
  • Index color con palette da 2,4 e 8 bit
  • True color a 15 bit + 1 di alpha
  • 4×4 Texel Compressed Texture (formato proprietario compresso)
  • Translucent texture A5I3: 5 bit per l’alpha (32 livelli, da completamente opaco a completamente trasparente), 3 bit per il colore (da una palette di 8 colori)
  • Translucent texture A3I5: 3 bit per l’alpha (8 livelli), 5 bit per il colore (palette da 32 colori)

Caratteristiche geometriche:

  • Max 2048 triangoli (6144 vertici) per frame @ 60 fps (circa 120.000 triangoli al secondo)
  • Supporta Triangoli, Quadrilateri, strip di triangoli e strip di quadrilateri
  • 4 luci in hardware
  • Transform & Lighting

Effetti:

  • Gouraud shading
  • Antialiasing
  • Edge marking
  • Toon shading
  • Fog
  • Stencil shadows

Per adesso è tutto. Nel prossimo articolo tratterò alcune tematiche relative allo sviluppo di videogames su altri tipi di piattaforme mobile, ovviamente parlo degli smartphone, e vedremo insieme quali sono i vantaggi e gli svantaggi di avere un sistema dotato di OS multitasking.

Vedremo inoltre come uno smartphone con una CPU da 500 MHz possa prendere sonore mazzate, nella grafica 2D, da un Nintendo DS con soli 66 MHz di clock.

Press ESC to close