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