www.codeworx.org/game-dev/Additives Shading im 16bit Modus

Additives Shading im 16bit Modus
Jarek Szpilewski

Ich höre mir hier gerade ein paar Musikstücke an, die ich vieleicht in mein Spiel aufnehmen werde. Und da ich nix anderes dabei zu tun habe, ausser vieleicht die GDC-Broschüre zu lesen (der Aufenthalt dort würde mich so ca. 5000$ kosten), werd ich mal ein kleines Tutorial für die DUS schreiben. Ich schreibe hier mal über etwas, dass ich noch so ziemlich gut aus dem Gedechtnis kann. Etwas was eigentlich jeder, der sich ernsthaft mit Grafikprogrammierung beschäftigt, kennen und können sollte - Additives Shading. Ich werde in diesem Tutorial verschiedene Methoden besprechen, wie man am besten und schnellsten Additives Shading programmiert.

So, fangen wir mal mit etwas ganz simplen an. Mit 50-50 Shading. Ihr wollt wissen, was das ist und wie man es macht? Beim 50-50 (oder 50%igen) Shading werden zwei Farben 50 zu 50 gemischt. Wie man das bewerkstelligt? In der Theorie geht´s ganz einfach. Man teilt die Pixelfarbe in die RGB-Bestandteile auf, halbiert sie, macht das Gleiche mit dem anderen Pixel, addiert dann die einzelnen RGB-Werte und errechnet die Farbe.

short Transparenz (short pixel1, short pixel2)
{
   int R1,G1,B1;
   int R2,G2,B2;
   int nR,nG,nB;
   short nColor;
   TeileRGB(pixel1,&R1,&G1,&B1);
   TeileRGB(pixel2,&R2,&G2,&B2);
   R1 = R1/2;
   G1 = G1/2;
   B1 = B1/2;
   R2 = R2/2;
   G2 = G2/2;
   B2 = B2/2;
nR = R1+R2; nG = G1+G2; nB = B1+B2;
   return CalcCol(nR,nG,nB);
}


Was fällt uns auf? Ja, es ist langsam. Sehr langsam. Und es ist primitiv. Die Chancen stehen gut, dass beim Spieler Falschfarben auf dem Bildschirm erscheinen. Das liegt am 16-Bit Farbmodell, welches so aufgebaut ist: RRRRRGGGGGGBBBBB - je 5 Rot-Bits, Blau-Bits und 6 Grün-Bits. Aber darum kümmern wir uns etwas später.
Es geht zuerst um Geschwindigkeit. In unserer C-Funktion wird viel Rechenzeit verschwendet. Das liegt auch mitunter daran, dass Divisionen und Multiplikationen auf 0x86-Prozessoren saulangsam sind. Additionen, Subtraktionen und Bitoperationen hingegen werden sehr schnell erledigt. Da setzen wir an. Ersetzen wir doch mal einfach die Divisionen in der TeileRGB() Funktion durch Bitoperationen. Dies ist sehr einfach. Ich klebe mal hier die Funktion Addtrans aus dem Quellcode von Bombafan ein. Diese Funktion entspricht unsere Funktion von oben, nur dass sie Falschafarben entfernt und etwas schneller ist.
unsigned short AddTrans(unsigned short col1,unsigned short col2)

//Addiert 2Pixel mit Additiven Shading (50-50 Mischung)
{
__asm { //Rot-Anteil extrahieren
     mov ax,col1;
     //Hier wird der wert von col1 gespeichert
      mov bx,col2;
     //Hier wird der wert von col2 gespeichert
     and ax,1111100000000000b;
     //Mit einer Bitmaske wird der Rotanteil
     and bx,1111100000000000b;
     //extrahiert.
     shr ax,11;
     //um 11 Stellen nach rechts shiften, damit
     shr bx,11;
     //wir auch den korrekten wert erhalten
     add ax,bx;
     //col1+col2 addieren und in ax speichern
     shr ax,1;
     //mögliche falschfarben eleminieren.
     shl ax,11;
     //nach links shiften, da Rotanteil immer zuerst steht
     mov wert,ax;
     //ax in wert speichern
     //Grün-Anteil extrahieren (wie oben)
     mov ax,col1;
     mov bx,col2;
     and ax,0000011111100000b;
     and bx,0000011111100000b;
     shr ax,5;
     shr bx,5;
     add ax,bx;
     shr ax,1;
     shl ax,5;
     add wert,ax;
     //Blauanteil extrahieren (wie oben)
     mov ax,col1;
     mov bx,col2;
     //Wir bruachen nicht shiften
     // da sich alles an der richtigen Stelle befindet
     and ax,0000000000011111b;
     and bx,0000000000011111b;
     add ax,bx;
     shr ax,1;
     add wert,ax;
  }
  return wert; //Palletenfarbe zurückgeben
}


Man könnte diese Funktion sicherlich auch in reinem C schreiben. Ob es allerdings immernoch so schnell wäre, weiss ich nicht. Aber wenn wir uns mal das 16-Bit Farbmodel anschauen, werden wir feststellen, dass die oben gezeigte Funktion doch etwas zu lang ist. Es geht noch kürzer und schneller. Schauen wir uns mal die Bitmasken für die Rot-, Grün- und Blauanteile an. Man könnte die Bitmasken doch zusammenlegen und daraus würde sich eine ultimative Bitmaske ergeben. Man bräuchte wesentlich weniger Zeilen Code.
Die optimierte Version:


unsigned short AddTrans(unsigned short col1,unsigned short col2)
{
   __asm
   {
     mov ax,col1
     mov bx,col2
     shr ax,1
     shr bx,1
     and ax,0111101111101111b
     and bx,0111101111101111b
     add ax,bx
     mov wert,ax
   }
return wert; }


Was will man mehr? Ja ,genau. Man will auch das Mischverhältnis bestimmen. Dazu benutze ich mal ausnahmsweise (fast) kein Assembler. Ich werde hier die Methode der vorberechneten Shading-Tabellen erklären. (Dieses Verfahren heisst übrigens Alpha-Blending.)
Was eine vorberechnete Shading-Tabelle ist? Eine vorberechnete Shading-Tabelle ist ein Speicher-Array, in dem sich alle möglichen Mischwerte für alle möglichen Pixelkombinationen mit einer Shading-Stufe befinden. In diesem Array werden auch die verschiedenen Shading-Stufen gespeichert.
Hier ein Beispiel:

WORD ShadeTab[32][65536];
Wir nehmen 32 Schattierungsstufen, was in den meisten Fällen ausreicht. Kommen wir nun zur Berechnung der Tabelle. Wie der Name schon sagt, werden die Werte vorberechnet, was auf langsamen PCs einige Zeit dauern kann.
Das ist die Funktion zur Vorberechnung:
void InitShade (void)
{
   int i, j;
   unsigned short r,g,b;
   unsigned short dr,dg,db;
   const double alpha[32] = { 0.0, 0.03, 0.06, 0.09,
   0.13, 0.17, 0.21, 0.24,
   0.27, 0.31, 0.34, 0.37,
   0.41, 0.44, 0.47, 0.49,
   0.51, 0.53, 0.56, 0.59,
   0.63, 0.66, 0.69, 0.73,
   0.76, 0.79, 0.83, 0.87,
   0.91, 0.94, 0.97, 1.0 };
   for(i=0;i<32;i++)
   {
     for(j=0;j<65536;j++)
     {
       GetRGB (r,g,b,j);
       dr = (int)(r*alpha[i]);
       dg = (int)(g*alpha[i]);
       db = (int)(b*alpha[i]);
       ShadeTab[i][j] = GetCol (dr,dg,db);
     }
  }
}

Diese Funktion setzt voraus, dass ShadeTab eine globale Variable ist. Nachdem die Tabelle vorberechnet wurde (möglichst am Anfang des Spieles), können wir sie nun benutzen. Dies geht ganz einfach. Mal angenommen wir wollen zwei Pixel miteinander mischen:
short wert = ShadeTab[16][pixel1] + ShadeTab[16][pixel2];
Dies ergibt ein 50%iges Mischverhältnis, welches man mit dem ersten Wert steuern kann. Dabei ist die Kombination 31/0 eine 100%ige Mischung. Das heisst, der zweite Pixel wird mit dem ersten überschrieben. Will man z.B. eine 70 zu 30 Mischung, so heissen die Werte 22/9 usw.
Aber nicht genug. Man kann nicht nur zwei Pixel mithilfe der ShadeTab addieren, man kann auch ganz einfach Fade-Effekte programmieren. Zum Beispiel einen Crossfade, wobei zwei Bilder stufenweise miteinander verschmelzen. Oder einen einfachen Fade2Black. Oder einen FadeBlack2Pic. Was nicht bzw. nur über Umwege geht ist z.B. ein Fade2White. Dieser ist trotzdem einfach zu realisieren. Man macht einfach einen Crossfade mit einem Speicherarray der mit Weisswerten gefüllt ist. Ich werde die Effekte hier nicht nochmal erklären, ihr könnt sie sicher selber coden. Und wenn nicht, dann versucht es. Ihr lernt dabei mehr, als wenn ihr irgendein Tutorial lesen würdet.
Hier noch zwei benötigte Funktionen:

void GetRGB (unsigned short &r,unsigned short &g,
unsigned short &b,unsigned short col)
//gibt das RGB-Tripple für den Farbwert "col" zurück
{
   unsigned short rot;
   unsigned short gruen;
   unsigned short blau;
   __asm
   {
     mov ax,col;
     and ax,1111100000000000b;
     shr ax,11;
     mov rot,ax;
     mov ax,col;
     and ax,0000011111100000b;
     shr ax,5;
     mov gruen,ax;
     mov ax,col;
     and ax,0000000000011111b;
     mov blau,ax;
   }
r = rot; b = blau; g = gruen;
}
unsigned short GetCol (unsigned short r,unsigned short g,
                       unsigned short b)
//errechnet Farbwert für das gegebene RGB-Tripple
{ unsigned short wert; __asm { mov ax,r shl ax,11 mov bx,g shl bx,5 add ax,bx mov bx,b add ax,bx mov wert,ax } return wert; }


So, zu diesem Thema gibt´s bestimmt noch andere Meinungen, Tutorials oder Methoden. Es ist aber nicht meine Aufgabe darüber zu richten, welche davon am besten ist. Ich habe hier einfach die Methoden vorgestellt, die ich auch in meinem aktuellen Spiel benutze.
Wenn ihr Fehler entdeckt oder nur etwas loswerden wollt, dann schreibt mir. (Ich hab das Tutorial grösstenteils aus dem Kopf geschrieben.)

Jarek@DigitalPayne.de