Tutorial BumpMap

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Bumpmapping

Vorwort

Bumpmapping, was ist das?

Das fragen sicherlich einige von euch. Also die Idee von Bumpmapping wurde schon 1978 von James Blinn entwickelt (obwohl es damals noch keine 3D Grafik gab???). Bumpmapping verleiht Mithilfe einer speziellen Textur einer Fläche eine richtige Oberfläche. Dabei wird aus den Farbwerten der speziellen Textur (Normalmap genannt) die Normalen der Fläche verändert und so entsteht der 3D-Effekt. Es gibt zwei (mir bekannte ) Arten von Bumpmapping. Einerseits das leicht veraltete Emboss Bumpmapping. Emboss-Bumpmapping funktioniert zwar auf fast jeder Grafikkarte, aber ergibt kein wirklich befriedigendes Ergebnis. Deshalb verwenden wir das neuere Dot-3 Bumpmapping. Dafür braucht man zwar eine neuere Grafikkarte (glaube mindestens Geforce-1), doch das Ergebnis sieht dann wirklich gut aus. So wird aus einem Quad ein Quad mit richtiger Oberfläche:

Dieses Einfache Quadrat wurde aus einer normalen Textur und einer Normalmap gerendert
Normale Textur
RGB Normalmap (bump_out.tga)

Erstellen von RGB Normalmaps

So erstmal genug Theorie, jetzt geht's los mit der Praxis. Als erstes müssen wir eine Normalmap erstellen. Dafür nutzten wir NormalMapGen von http://www.developers.nvidia.com. Wenn man jetzt z.B. aus der Datei bump_in.tga eine Normalmap machen will, startet man es wie folgt:

NormalMapGen.exe bump_in.tga bump_out.tga

Für die Leute, die nicht aus DOS-Zeiten stammen oder nicht wissen wie man die Konsole bedient, genauer:

  • Unter "Start->Programme->MS-DOS-Eingabeaufforderung" die Konsole starten
  • Jetzt muss da irgendwas wie C:\Windows\ stehen (variiert von System zu System)
  • In dem man erst das Laufwerk mit Doppelpunkt und dann cd (Verzeichnis) eingibt, in das Verzeichnis wechseln, wo NormalMapGen.exe ist. Bei mir:
- D:
- cd \Tools\NormalMapGen\
  • Wie man sieht liegt bei mir NormalMapGen im Verzeichnis "D:\Tools\NormalMapGen\"
  • Dann einfach wie oben das Programm starten. Dabei muss Bump_in.tga im gleichen Verzeichniss sein:
NormalMapGen Bump_In.tga Bump_out.tga 
  • Es versteht sich von selbst, dass die Bilder anders heißen können.

Jetzt wird aus bump_in.tga eine RGB Normalmap wie oben gemacht. Die In-Datei enthält die Graustufen, so wie die Normalmap später aussehen soll. Die Eingangsdatei muss bestimmte Kriterien erfüllen. Weil Programmierer aber faul sind, wiederhol ich die nicht alle (stehen in der Readme von NormalmapGen).

Direktlink zum Tool : http://developer.nvidia.com/view.asp?IO=map_generator
Normalmapgenerator als Plug-In für Photoshop : http://developer.nvidia.com/object/photoshop_dds_plugins.html

Initialisierung

Wenn ihr dann so eine schön bunte Normalmap habt, muss die ja auch irgendwie in das Programm. Jetzt solltet ihr erstmal nur weiterlesen, wenn ihr schon mal was von Multitexturing gehört habt, denn das braucht ihr jetzt (Tutorials gibt's dazu in unsrer Tutorialsektion, Tutorial Nr.5). Zuerst müssen wir überprüfen, ob die Grafikkarte Multitexturing unterstützt.

glGetIntegerv(GL_MAX_TEXTURE_UNITS_ARB, @TMUs);
if TMUs < 2 then
 begin
 ShowMessage('Grafikkarte unterstützt Multitexturing nicht');
 Close;
 end;

Dann noch Combiner:

glGetIntegerv( GL_MAX_GENERAL_COMBINERS_NV, @MaxCombiners);
ReadExtensions;
ReadNVREGCOMBExtension;
if not (Assigned(@glCombinerParameteriNV) and Assigned(@glActiveTextureARB)) then
  begin
  ShowMessage('Grafikkarte unterstützt Combiner nicht');
  Close;
  end;

Die beiden Befehle in Zeile 2 und 3 brauchen die Unit NVREGCOMB. Das war auch schon alles für die GLInit Funktion. Dann müsst ihr noch die Texturen laden, die ihr braucht (sollten am Anfang nur zwei sein).

Und jetzt ... ?

Bevor wir jetzt dem eigentlichen Programmieren zuwenden, müssen wir noch überlegen, was überhaupt gemacht werden soll. Bevor wir das können, müssen wir uns überhaupt im Klaren sein, wie so eine Grafikkarte, genauer die Register, funktionieren (so mit Strom, oder?):

Tutorial BM bump 05.jpg

Hier sieht man den Aufbau des Registers in unserem Fall. Halt! Was ist ein Register? Ein Register ist der Teil der Grafik-Pipeline, bei dem die genaue Farbe jedes zu zeichnenden Pixels bestimmt wird. Der Register ist also fast das letzte Stück in der Grafik-Pipeline. Danach wird das Bild auf den Bildschirm gebracht. Doch was ist das da oben für ein Bild? Dort sieht man das Funktionsprinzip in unserem Fall. Links stehen alle Inputs. Es wird für jeden Pixel auf dem Bildschirm die Texturfarbe der normalen Textur und der Bumpmap angegeben. Hinzu kommt die Diffuse und Specular Farbe. Zuerst geht ein RGB und ein Alpha Wertder Bumpmap-Textur und ein RGB und ein Alpha Wert der Diffuse-Farbe in den ersten Combiner. Dort werden beide kombiniert (wie der Name schon sagt) und heraus kommt ein RGB und ein Alpha Wert. Diese werden beide in der Zwischenvariable Spare 0 gespeichert. Jetzt könnte man noch weitere Combiner für andere Effekte einbeziehen, doch das ist nicht meine Aufgabe. Wenn man jetzt alles benötigte berechnet hat, geht Spare 0 mit der Specular-Farbe und einem RGB und ein Alpha Wert zusammen in den Final-Combiner. Dort werden die alle nochmal zusammen gerechnet und das ganze geht über Drähte ab zum Bildschirm!!! Doch jetzt hab ich so oft das Wort Combiner benutzt ohne es wirklich zu erklären. Das hole ich jetzt nach:

Tutorial BM bump 06.jpg

Also ein Combiner ist ein Teil des Registers, der Verschiedene Eingaben in Form von Farbwerten, zusammenrechnet. Bei uns gibt es nur zwei Eingaben, es können aber z.B. bei einer Geforce-3 bis zu vier pro Combiner sein. Wenn wir schon mal bei technischen Details sind. Bei einer Geforce-3 gibt es 8 Combiner. Die Anzahl der Combiner, die eure Grafikkarte unterstützt, bekommt ihr mit

glGetIntegerv( GL_MAX_GENERAL_COMBINERS_NV, @MaxCombiners);

raus, wobei MaxCombiners eine Integer-Zahl ist.

Pflicht

Da jetzt (hoffentlich) alle das Prinzip verstanden haben, können wir uns jetzt an das echte Progarmmieren machen. Als erstes müssen wir die beiden Texturen einbinden:

{         Textur         }
glActiveTextureARB(GL_TEXTURE1_ARB);
glEnable(GL_TEXTURE_2D);
Textures[1].Bind;

{        Bumpmap      }
glActiveTextureARB(GL_TEXTURE0_ARB);
glEnable(GL_TEXTURE_2D);
Textures[0].Bind;

glEnable(GL_REGISTER_COMBINERS_NV);

Die letzte Zeile aktiviert die Combiner. Das sollte alles (bis auf die letzte Zeile) nichts Neues sein. Jetzt haben wir die Normalmap in der ersten und die normale Textur in der zweiten Texturunit der Grafikkarte. Als nächstes bestimmen wir wie viele Combiner verwendet werden sollen:

glCombinerParameteriNV(GL_NUM_GENERAL_COMBINERS_NV, 1);


Für den Anfang reicht erst mal einer. Für besondere Effekte braucht man später mehr. Als nächstes müssen wir der Grafikkarte mitteilen, wie sie das Ganze mischen soll. Zuerst rechnen wir bei einem Combiner Bumpmap und RGB Farbe zusammen. Letztere brauchen wir später, wenn dann Licht dazukommt:

glCombinerInputNV(
    GL_COMBINER0_NV,            //Combiner, der benutzt wird
    GL_RGB,
    GL_VARIABLE_A_NV,           //Variable beim Combiner
    GL_TEXTURE1_ARB,            //Wert: Bumpmap Textur
    GL_EXPAND_NORMAL_NV,
    GL_RGB
  );

  glCombinerInputNV(
    GL_COMBINER0_NV,            //Combiner, der benutzt wird, wie oben
    GL_RGB,
    GL_VARIABLE_B_NV,           // Variable beim Combiner
    GL_PRIMARY_COLOR_NV,        //Wert: RGB Wert der Oberfläche
    GL_EXPAND_NORMAL_NV,
    GL_RGB
  );


So, was haben wir hier gemacht? Zuerst haben wir der Variable A des ersten Combiners die Bumpmap-Textur zugewiesen. Der Variable B des gleichen Combiners haben wir dann die Farbe zugewiesen, die man mit glColor bestimmt. Die Farbe braucht man später für die Lichtrichtung. Alles was nicht kommentiert ist, solltet ihr erst mal so lassen. Soweit zum Input.

glCombinerOutputNV(
    GL_COMBINER0_NV,
    GL_RGB,
    GL_SPARE0_NV,       // Output von A und B
    GL_DISCARD_NV,      // Output von C und D
    GL_DISCARD_NV,      // Summe von allen Variablen
    GL_NONE,            // Skalierung
    GL_NONE,            // Bias
    GL_TRUE,            // Ja/Nein Output von A und B
    GL_FALSE,           // Ja/Nein Output von C und D
    GL_FALSE            // Muxsum
  );

Jetzt brauchen wir noch den Output. Die ersten beiden Zeilen sollten sich von selbst erklären. Dann definiert man wohin das Ergebnis von A und B gespeichert werden soll. Wir speichern das erstmal in einer temporären Variablen GL_SPARE0_NV. Die nächsten beiden Zeilen sind für den Output anderer Ergebnisse zuständig und interessieren erstmal nicht. Skalieren und Bias sind zu irgendwelchen Transformationen da. Als nächstes bestimmen wir, was ausgegeben wird. Die letzte Zeile kenne ich selber nicht. Als letztes nur noch bestimmen wie der Final-Combiner arbeiten soll, und dann sind wir fertig:

glFinalCombinerInputNV(GL_VARIABLE_A_NV, GL_SPARE0_NV, GL_UNSIGNED_IDENTITY_NV, GL_RGB);

glFinalCombinerInputNV(GL_VARIABLE_B_NV, GL_TEXTURE0_ARB, GL_UNSIGNED_IDENTITY_NV, GL_RGB);

glFinalCombinerInputNV(GL_VARIABLE_C_NV, GL_ZERO, GL_UNSIGNED_IDENTITY_NV, GL_RGB);

glFinalCombinerInputNV(GL_VARIABLE_D_NV, GL_ZERO, GL_UNSIGNED_IDENTITY_NV, GL_RGB); 

glFinalCombinerInputNV(GL_VARIABLE_E_NV, GL_ZERO, GL_UNSIGNED_IDENTITY_NV, GL_RGB); 

glFinalCombinerInputNV(GL_VARIABLE_F_NV, GL_ZERO, GL_UNSIGNED_IDENTITY_NV, GL_RGB);


Das ist alles nicht schwer zu verstehen. Uns interessieren nur die ersten beiden Zeilen. Sie weisen der Variable A des Final-Combiners die Variable GL_SPARE0_NV und der Variable B die normale Textur. Die anderen Variablen werden als leer deklariert. Man muss die vier letzten Zeilen nicht schreiben (ich hab's nur der Ordnung wegen gemacht). Der restliche Code erzeugt nur noch ein Quad mit zwei Texturen.

Kür

Da das Quad zwar schon richtig gut aussieht, aber noch ein bisschen statisch wirkt wollen wir uns jetzt einer kleinen Verbesserung zuwenden: Das Licht. Die Lichtrichtung gibt man einfach über glColor an. Dabei muss man jedoch immer relativ von der Fläche ausgehen. Also rot entspricht der x, grün der y und blau der z Achse. Ach ja, rot 0 heißt z.B. das das Licht von links kommt, 0.5 von der Mitte und 1 von rechts. Das gleiche nur von oben nach unten gilt für grün. Mit Blau ist das so'ne Sache. Kleiner als 0.5 sollte es nie werden, weil sonst das Quad vollkommen schwarz wird. Ein guter Wert liegt zwischen 0.8 und 1.

Tutorial BM bump 07.jpg

Nochmal zurück zur Berechnung: wie schon gesagt das alles muss man relativ zu dem Polygon ausrechnen. Weil ein paar andere Leute und ich aber noch keine Möglichkeit gefunden haben dieses Licht so zu berechnen, kann ich euch leider nicht sagen, wie das funktioniert. Das ihr in dem Sample trotzdem die Wirkungsweise bestaunen könnt, wird einfach eine "Drehung" des Lichtes mit folgendem Codeschnipsel simuliert:

procedure InitLight;
  var
    light_position: TVertex;
  begin
    light_position[0] := cos(lightangle);
    light_position[2] := 1;
    light_position[1] := sin(lightangle);
    NormalizeVector(light_position);
    glColor3fv(@light_position[0]);
  end; (*InitLight*)

Die Wirkungsweise sollte eigentlich klar sein: Mit einem Sinus und Cosinus einer Variable, die ständig erhöht wird, wird eine gleichmäßige Bewegung simuliert.

Nachwort

So das war auch schon mein erstes Tutorial. Das Ganze ist nicht einfach, doch wenn man es mal verstanden hat, geht's und es gibt sogar ganze ordentliche Ergenbisse. Es gibt jetzt sehr viele Möglichkeiten das Ergebnis zu modifizieren. Wen das ganze interessiert sollte unbedingt auf Jan Horns Homepage gehen (http://www.sulaco.co.za). Dort gibt's u.a. ein Sample für Bumpmapping. Auch auf http://developer.nvidia.com gibt's mehr zum Thema.


MfG HomerS

Dateien


Vorhergehendes Tutorial:
Tutorial_Partikel1
Nächstes Tutorial:
Tutorial_Bumpmaps_mit_Blender

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.