Dieser Artikel erschien erstmals im PC Magazin 7/2001. Die Wieder­veröffentlichung erfolgt mit freundlicher Genehmigung der WEKA Media Publishing GmbH.

Diesen Artikel im đŸ’Ÿ PDF-Format herunterladen

Fortschrittliche Rendertechniken: Bumpmapping

Licht in Echtzeit

Mit Bumpmapping verstÀrken Sie den realistischen Eindruck von 3D-Grafiken. Komplexe und detailreiche OberflÀchen tÀuschen Wirklichkeit vor.

Carsten Dachsbacher

3D-Hardware-Entwickler bieten stĂ€ndig neue Optionen an, die die 3D-Grafik-Programmierer ausfĂŒllen mĂŒssen. Dazu gehört auch das von moderner Hardware unterstĂŒtzte Bumpmapping in OpenGL: ein Verfahren, das den realistischen Eindruck von 3D-Objekt­oberflĂ€chen unterstreicht. Anders als Texture-Mapping, das auf die Farbe der Objektober­flĂ€chen abzielt, wird Bumpmapping dazu verwendet, Unebenheiten der OberflĂ€chen­struktur zu rendern. Im Bild unten sehen Sie einen Torus als Drahtgitter­modell, texturiert und mit Bumpmapping.

EIN OBJEKT als Drahtgittermodell mit und ohne Bumpmapping
EIN OBJEKT als Drahtgittermodell mit und ohne Bumpmapping

Mit Bumpmapping können Sie Beulen auf der OberflĂ€che von 3D-Objekten darstellen. Objekte in einer so hohen geometrischen Auflösung zu rendern, um solche Effekte zu erzielen, ist sehr rechenzeit- und speicher­intensiv. Abgesehen davon, sind die Unebenheiten im Vergleich zur groben geometrischen Form eines Objekts sehr klein. Nehmen Sie als Beispiel das 3D-Modell eines Holztisches. Die Unregel­mĂ€ĂŸigkeiten auf der TischflĂ€che sind klein im Vergleich zur ihrer ebenen Form. Deshalb liegt es nahe, nicht die Geometriedaten selbst so fein zu gestalten.

Theorie des Bumpmapping

DIE ZUSAMMENSETZUNG der OberflÀchenbeleuchtung
DIE ZUSAMMENSETZUNG der OberflÀchenbeleuchtung

Der wichtige Punkt beim Bumpmapping ist: Nur die Beleuchtungs­berechnung lĂ€sst die Unebenheiten sehen. Diese sind geometrisch nicht im Dreiecksnetz vorhanden. An den geraden Kanten eines mit Bumpmapping gerenderten 3D-Objekts sehen Sie, dass dessen Form selbst nicht verĂ€ndert wird.

Die Idee des Bumpmapping wurde 1978 von James Blinn entwickelt. Bumpmapping ist ein rein textur­basierendes Rendering-Verfahren, um Unebenheiten auf OberflĂ€chen durch die Beleuchtung zu simulieren. Die Unebenheiten werden in einer Graustufen­textur (Graustufen-Bitmap) als Heightfield angegeben, deren Auswirkung Sie im Bild sehen.

Der Grafiker schafft nur die Graustufen-Bitmap. Daraus generiert der Programmierer Daten, wie diese fĂŒr das verwendete Bumpmapping-Verfahren nötig sind. Von diesen Verfahren stellen wir eines vor, dass neuere Hardware wie die GeForce GPUs von nVidia benötigt. Anschließend zeigen wir Ihnen einen relativ alten Ansatz, der auf jeder 3D-Hardware funktioniert.

EINE OBERFLÄCHE wird durch ein Heightfield verĂ€ndert.
EINE OBERFLÄCHE wird durch ein Heightfield verĂ€ndert.

Die Theorie der Beleuchtungs­berechnung zeigt, wo das Bumpmapping ansetzt. Beleuchtung berechnen Sie aus Formeln, welche Sie mit der Vektorrechnung darstellen und verdeutlichen. Mit einer vereinfachten Formel, berechnen Sie diffuse und spiegelnde Reflexionen. Diese Formel entstammt dem Blinn-Beleuchtung­smodell, das wie das Phong-Modell empirisch ermittelt wurde.


C = (max(0,(L*N))
	+ max (0,(H*N))^n)
	x Dl x Dm
		

Blinn und Phong sind als Grundlagen­forscher der Grafik­programmierung berĂŒhmt. Dl ist die Farbe des Lichts, Dm die Farbe der OberflĂ€che an der betrachteten Stelle. Diese OberflĂ€chen­farbe kann aus einer Textur ausgelesen sein. Der Potenzwert n bestimmt die GrĂ¶ĂŸe der Glanzlichter. GrĂ¶ĂŸere Werte bedeuten kleinere Glanzlichter der spiegelnden Reflexion. Die vorkommenden Vektoren bezeichnen mit
‱ L: die einfallende Lichtrichtung, mit
‱ N: die Normale am OberflĂ€chen­punkt und mit
‱ H: den so genannten Halfangle Vektor. Letzterer hĂ€ngt auch von der Position des Punktes auf der OberflĂ€che und der Lichtquelle ab.

Wenn Sie sich obige Blinn-Formel genauer ansehen, fÀllt auf, dass es zwei Wege gibt, die OberflÀche nicht entsprechend der geometrischen Vorgaben, also nach dem Dreiecksnetz, darzustellen.
‱ Der erste Ansatzpunkt: Verschieben Sie die Punkte der OberflĂ€che. Diese Technik nennt sich Displacement-Mapping und funktioniert fĂŒr heutige 3D-Hardware nicht in Echtzeit.
‱ Die zweite Variante, das Bumpmapping, setzt an der OberflĂ€chen­normalen an.

FĂŒr ein 3D-Objekt verwenden Sie eine Textur, aus der die Farbwerte Dm fĂŒr die OberflĂ€che gespeichert sind, und eine oder mehrere Bumpmaps, die die Perturbation (die Änderung der OberflĂ€chen­normalen) enthĂ€lt.

Mit den aktuellen 3D-Grafikkarten lĂ€sst sich die Beleuchtung fĂŒr jeden gerenderten Pixel in Echtzeit berechnen.

Dot Product Bumpmapping

FĂŒr das Dot Product Bumpmapping Verfahren benötigen Sie moderne GPUs. Es basiert auf Bumpmaps, die als RGB-Texturen gespeichert werden. Die RGB-Werte eines Texels (zwischen 0 und 255) reprĂ€sentieren die x-, y- und z-Komponenten eines Vektors im Intervall [-1, 1]. Solche Bumpmaps können Sie sich aus Heightfields erzeugen lassen. Sie können ein Tool von nVidia (inklusive Sourcecode) downloaden, um RGB-Normal-Maps aus Heightfields zu generieren. Dieses Werkzeug finden Sie unter den Developer-Informationen auf der nVidia-Homepage zum freien Download: www.nvidia.com. Die Komponenten der Normalen­vektoren werden durch Ableiten des Heightfields berechnet. Die zentrale Operation bei der Beleuchtungs­berechnung des Bumpmappings und der diffusen Beleuchtung ist das Skalarprodukt aus der Normalen und des Vektors vom OberflĂ€chen­punkt zur Lichtquelle:

N * L

Diese Formel entspricht dem Lambertschen Gesetz. Es ist egal, in welchem Koordinaten­raum die beiden Vektoren angegeben sind, es muss aber beides mal der selbe sein. Doch welcher Raum soll das sein und in welchem ist die Normale angegeben? Die Antwort darauf gibt das Tangent Space Bumpmapping.

Der entscheidende Koordinaten­raum ist der so genannte Tangent Space. Diesen drei­dimensionalen Raum geben Sie durch eine 3-x-3-Matrix an, deren drei Spalten­vektoren den Raum aufspannen. Sie benötigen fĂŒr jeden Vertex Ihres 3D-Modells einen Tangent Space. Die Normale des 3D-Modells am Vertex wĂ€hlen Sie als +z-Achse, also als dritten Spaltenvektor. Durch den Vertex und seine Normale ist eine Ebene definiert, die sich tangentiell zur OberflĂ€che befindet, daher der Name Tangent Space.

Sie brauchen noch zwei weitere Vektoren, um den Raum aufzuspannen. WĂ€hlen Sie zum Beispiel die +y-Achse des Modelspace (des Koordinaten­raumes, in dem Ihr 3D-Modell definiert wurde) oder einen Vektor, den Sie durch die implizite Beschreibung einer OberflĂ€che erhalten. Im Beispiel­programm finden Sie dafĂŒr einen Torus. Der noch fehlende dritte Vektor ergibt sich aus dem Kreuzprodukt der beiden anderen. Normalerweise werden die Vektoren so konstruiert, dass sie in der Tangential­ebene an der OberflĂ€che liegen.

Nun haben Sie zu jedem Vertex einen Tangent Space definiert, den Sie fĂŒr das Rendern speichern mĂŒssen. Die folgenden Schritte mĂŒssen Sie wĂ€hrend der Laufzeit des Programms erledigen. Interpretieren Sie Ihr Heightfield so, dass die Höhen­information eine Verschiebung entlang der +z-Achse des Tangent Space bewirkt. Sie transformieren den Vektor zur Lichtquelle in den Tangent Space: Wenn Sie Ihr 3D-Modell rendern, generieren Sie auf dem Matrix-Stack von OpenGL eine Reihe von Trans­formationen. Sie benötigen die inverse Transformation. Dazu invertieren Sie entweder die resultierende ModelView-Matrix, oder Sie erzeugen eine Matrix mit den einzelnen invertierten Transformations­schritten in umgekehrter Reihenfolge. Wenn Sie mit dieser inversen Matrix die Position der Lichtquelle in Ihrer 3D-Welt transformieren, erhalten Sie einen Ortsvektor, der die Position der Lichtquelle im Modelspace beschreibt.

DIE RGG-WERTE in den sechs 2D-Texturen der Cubemap reprÀsentieren normalisierte Vektoren.
DIE RGB-WERTE in den sechs 2D-Texturen der Cubemap reprÀsentieren normalisierte Vektoren.

Als letzten Schritt berechnen Sie den Vektor eines jeden Vertex zur Lichtquelle (in Modelspace-Koordinaten) durch Subtraktion und transformieren diesen Vektor L in den Tangent Space. Die Transformation in den Tangent Space erfolgt durch das Skalarprodukt aus dem L-Vektor und jedem der Spalten­vektoren.

Beim Rendern eines Dreiecks durch die 3D-Hardware werden die Normalen als RGB-Tripels behandelt und linear perspektivisch korrekt interpoliert. Die L-Vektoren können sich in unter­schiedlichen Tangent Spaces befinden, denn jeder Vertex des Dreiecks hat seinen eigenen Tangent Space. Die 3D-Hardware routiert gewissermaßen die L-Vektoren von einem Raum in den nĂ€chsten.

Eine mathematisch korrekte Beleuchtungs­berechnung mĂŒsste diese Vektoren fĂŒr jeden Pixel normalisieren, da sich ihre LĂ€nge bei der linearen Interpolation der Vektor-Komponenten Ă€ndert.

DafĂŒr bietet sich Cube Mapping an: Das ist eigentlich eine Form des Texture-Mapping, die einen unnormal­isierten Vektor verwendet, um eine Textur zu adressieren. Diese besteht aus sechs quadratischen 2D-Bitmaps, die wie die FlĂ€chen eines WĂŒrfels angeordnet sind. So sehen Sie, wie ein Vektor einen Pixel adressiert.

CUBE MAPPING adressiert sechs 2D-Bitmaps mit unnormalisierten Vektoren.
CUBE MAPPING adressiert sechs 2D-Bitmaps mit unnormalisierten Vektoren.

Die Komponente mit dem grĂ¶ĂŸten Betrag und ihr Vorzeichen bestimmen, welche Seite des WĂŒrfels getroffen wird. Die 2D-Koordinaten auf der WĂŒrfelseite erhalten Sie, indem Sie die beiden kleineren Komponenten durch die GrĂ¶ĂŸte dividieren. Ein RGB-Tripel, das durch die Interpolation der Normalen im Tangent Space entsteht, wird als Vektor interpretiert. Dieser Vektor schneidet den WĂŒrfel an einer bestimmten Stelle. Die Lage des Schnittpunkts ist unabhĂ€ngig von der LĂ€nge des Vektors, nur die Richtung ist entscheidend.

Sie können die Cubemap-Texturen so vorberechnen, dass an jeder Stelle ein bestimmtes RGB-Tripel gespeichert ist: das RGB-Tripel, das dem normalisierten Vektor entspricht. Im ĂŒbrigen werden Cubemaps dazu verwendet, Licht-Reflexionen oder -Refraktionen (Lichtbrechung) darzustellen.

UNSER DOT-3-BUMPMAPPING Programm in Aktion
UNSER DOT-3-BUMPMAPPING Programm in Aktion

Seit 1978 haben Entwickler daran gearbeitet, das von Blinn formulierte Bumpmapping in 3D-Hardware zu integrieren. In unserem Beispiel­programm finden Sie die Implementation und FortfĂŒhrung der hier gezeigten Verfahren. Mit dieser Vorarbeit können Sie zur Ansteuerung der GeForce-Karte ĂŒbergehen.

Register Combiners

GeForce-, Quadro- und neuere nVidia-Karten besitzen Register-Combiners. Damit lĂ€sst sich die Farbberechnung fĂŒr jeden Pixel konfigurieren. Beachten Sie den Unterschied zwischen Konfigurieren und Programmieren: ersteres ist Einstellen, letzteres freies Gestalten. Dieses erlauben erst die Pixelshader der neuesten Karten­generationen. Die Register-Combiners ersetzen, wenn Sie sie aktivieren, die Standard-OpenGL-Rendering­optionen. Sie sind deutlich komplexer und flexibler. Die Register-Combiners steuern Sie ĂŒber OpenGL Extensions. Diese sind in der neuesten Version der Datei glext.h definiert, die Sie auch bei unserem Beispiel­programm finden. Wie Sie die Funktionen nutzen, entnehmen Sie dem Beispiel­programm. Auf den Entwickler­seiten von nVidia finden Sie die genauen Spezifi­kationen und Dokumen­tationen aller Features.

Dot-3-Bumpmap-Texturen

AUS EINEM HEIGHTFIELD wird eine RGB-Normalmap.
AUS EINEM HEIGHTFIELD wird eine RGB-Normalmap.

Um eigene Bumpmaps fĂŒr Dot-3-Bumpmapping zu generieren, beginnen Sie mit einem Heightfield, also einer Graustufen-Bitmap. Hellere Graustufen bedeuten, dass die so gekenn­zeichnete OberflĂ€che mehr nach außen geschoben wird. Eine solche Bumpmap-Textur wandeln Sie mit dem nVidia-Bumpmap-Tool in eine RGB-Normal Map um:

normalmapgen.exe height.tga bump.tga

Bevor Sie die Maps in OpenGL laden, generieren Sie Mipmaps. Das sind niedrigere Auflösungs­stufen einer Textur, um hĂ€ssliche Effekte beim Rendern zu vermeiden. In der Textur befinden sich vorzeichen­behaftete Vektoren, die nur als RGB-Werte gespeichert sind. Das weiß die gluBuild2DMipmaps(...)-Funktion von OpenGL nicht, die automatisch Mipmaps generiert. Da diese fĂŒr diesen Zweck unbrauchbar sind, mĂŒssen Sie eigene Mipmaps generieren, also eine Funktion implementieren, die die Auflösung einer RGB-Normalmap halbiert! Dazu speichern Sie jeden Pixel der RGB-Normal in folgender Struktur, die die Vektor-Komponenten und seine LĂ€nge enthĂ€lt:


typedef struct
{
	unsigned char nz, ny, nx, mag;
} DOT3NORMAL;

DOT3NORMAL bumpmap[SIZE*SIZE];
		

nx, ny und nz initialisieren Sie jeweils mit den RGB-Werten, mag mit dem Wert 255. Bei der Halbierung der Auflösung fassen Sie vier benachbarte Pixel, die in einem Quadrat angeordnet sind, zu einem neuen zusammen. Die Komponenten der Vektoren a, b, c und d mĂŒssen Sie vom Wertebereich [0,255] auf der Intervallskala [-1,1] verschieben und skalieren. Die Werte innerhalb des Intervalls multiplizieren Sie mit der LĂ€nge des ursprĂŒnglichen Vektors und summieren sie auf. Damit erhalten Sie einen neuen Vektor, den Sie erneut normalisieren und als RGB-Tripel in der neuen Mipmap-Stufe speichern. ZusĂ€tzlich speichern Sie vorher seine LĂ€nge in mag. Der Code fĂŒr einen Pixel sieht so aus:


//a,b,c,d: Texel in bumpmap[]
// angeordnet als
// a b
// c d
DOT3NORMAL a, b, c, d;
DOT3NORMAL neu;
VERTEX n;
n.x = (a.nx / 127 - 1) * a.mag / 255;
n.x += (b.nx / 127 - 1) * b.mag / 255;
n.x += (c.nx / 127 - 1) * c.mag / 255;
n.x += (d.nx / 127 - 1) * d.mag / 255;
...

l = lengthVector(n);
normVector(n);
neu.nx = 128 + 127 * n.x;
...

neu.mag = min(255, 255 * l * 0.25);
		

Die so berechneten Mipmap-Stufen ĂŒbergeben Sie mit glTexImage2D(...) an OpenGL. Wenn Sie alles zusammenfassen und mit den Implementierungs­details ausstatten, erhalten Sie unser fertiges Dot-3-Bumpmapping-Programm.

Emboss-Bumpmapping

EMBOSSING bei Bumpmaps.
EMBOSSING bei Bumpmaps.

Nun gibt es noch ein sehr altes, anderes Verfahren, um Bumpmapping darzustellen. Das Emboss-Bumpmapping ist auf jeder 3D-Karte einsetzbar. Durch diesen Fakt ließen sich schon manche 3D-Karten­hersteller zur Behauptung verleiten, ihre 3D-Karten wĂŒrden Bumpmapping in der Hardware unterstĂŒtzen. Diese Methode ist mit den Embossfiltern in Bildbearbeitungs­programmen verwandt. In bestimmten FĂ€llen sind beim Emboss-Bumpmapping Darstellungs­artefakte durch Unterabtastung zu sehen, die als unscharfe Bewegungen erscheinen. Wenn Sie unser Beispiel­programm dazu ausprobieren, werden Sie sehen, dass sich der Einsatz aber auf jeden Fall lohnen kann.

Das Verfahren lĂ€sst nur die Approximation der diffusen Beleuchtungs­komponente zu, womit sich die vorige Formel fĂŒr die Beleuchtungs­berechnung auf folgende Terme reduziert:

C = ((L * N)) x Dl x Dm

Diese Formel hat gewaltig gegenĂŒber der Blinn’schen-Ausgangsformel an KomplexitĂ€t verloren: Es fehlen nicht nur die Rechen­operationen, sondern auch der Halfangle-Vektor, den Sie fĂŒr das Dot-3-Bumpmapping benötigten. Die Bumpmap, die wir fĂŒr das Emboss-Bumpmapping einsetzen, ist eine Höhen­information (Heightfield/Graustufen-Bitmap): Wie das erste Bild zeigte, reprĂ€sentiert ein Pixel in der Bumpmap eine Höhen­verschiebung auf der OberflĂ€che.

UNSER BEISPIELPROGRAMM fĂŒr Emboss-Bumpmapping
UNSER BEISPIELPROGRAMM fĂŒr Emboss-Bumpmapping

Wir betrachten das Verfahren zunĂ€chst im Ein­dimensionalen, also mit einer Zahlenreihe, die einen Höhenverlauf darstellt. Wenn Ihnen die erste Ableitung einer Folge von Höhenwerten vorliegt, entspricht diese der Steigung am entsprechenden OberflĂ€chen­punkt. Diese Steigung m wird verwendet, um einen Basisfaktor Fd fĂŒr die diffuse Beleuchtung zu erhöhen oder zu erniedrigen. Die Summe (Fd+m) approximiert den Term (L*N).

Als nĂ€chstes approximieren Sie die Steigung. Lesen Sie die Höhe H0 des OberflĂ€chen­punktes aus der entsprechende Stelle der Heightmap, was spĂ€ter die 3D-Hardware fĂŒr Sie erledigen wird. Lesen Sie die Höhe erneut aus, wobei Sie die Bumpmap ein kleines StĂŒckchen in Richtung der Lichtquelle verschieben, und Sie erhalten H1. Rechnen Sie diese Verschiebung aus. Die Differenz aus H0 und H1 ergibt: m = H1 - H0.

Die Textur verschieben Sie, indem Sie die Textur­koordinaten modifizieren. Die Modifikation berechnen Sie wieder im Tangent Space. Dazu transformieren Sie die Lichtquelle in den Modelspace. Bilden Sie die Skalarprodukte des Vektors von einem Vertex zur Lichtquelle und der Tangente sowie der Binormalen des Tangent Space. Damit erhalten Sie zwei Verschiebung­swerte, die Sie zur ursprĂŒnglichen Texture-Koordinaten addieren.

Wenn Sie die Texturen und Bumpmaps in OpenGL geladen haben, fĂŒhren Sie das Emboss-Bumpmapping in drei Renderpasses durch. Diese Variante funktioniert auf jeder OpenGL-Hardware, die Texture-Mapping unterstĂŒtzt.
‱ Im ersten Renderpass verwenden Sie die Bumpmap-Textur mit den Original-Textur­koordinaten und deaktivieren die OpenGL-Beleuchtungs­berechnung und das Blending.


glBindTexture(GL_TEXTURE_2D, bumpTex);
glDisable(GL_BLEND);
glDisable(GL_LIGHTING);
renderObject();
		

‱ Im zweiten Schritt erhalten Sie die 3D-Objekte mit fertiger Beleuchtung, jedoch ohne Farbe. Dazu wĂ€hlen Sie die invertierte Bumpmap-Texture, Blending mit GL_ONE/GL_ ONE und den berechneten verschobenen Textur­koordinaten:


glBindTexture(GL_TEXTURE_2D, invBumpTex);
glBlendFunc(GL_ONE, GL_ONE);
glDepthFunc(GL_LEQUAL);
glEnable(GL_BLEND);
renderObjectEmboss();
		

‱ Im dritten Renderpass kommt Farbe durch die Farbtextur und die OpenGL-Beleuchtung ins Spiel. Dazu verwenden Sie folgende Einstellungen:


glBindTexture(GL_TEXTURE_2D, textureMap);
glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);
glEnable(GL_LIGHTING);
renderObject();
		

Probieren Sie die High-End-Render­techniken aus. Wenn Sie Ihre 3D-Grafik mit den Bumpmapping-Features ausstatten, werden Sie feststellen, wie realistisch bisher flache, kĂŒnstlich anmutende 3D-Objekte auf den Betrachter wirken können.

Um die mathematische Arbeit von James Blinn zu studieren, verweisen wir auf die nachfolgenden Literatur­angaben. Diese Grundlagen fĂŒr die Berechnung von 3D-RĂ€umen wurden erst in den letzten Jahren gelegt. Die komplexe mathematische Materie ist noch nicht vollstĂ€ndig erforscht.