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

Genesis-Projekt: Landschaften texturieren/Spezialeffekte

Atmosphäre und Panorama

Mit unserem Beispiel­programm erforschen Sie berechnete Landschaften. Lassen Sie sich von der realistischen Darstellung beeindrucken.

Carsten Dachsbacher

Mit der OpenGL-API haben Sie in der letzten Ausgabe Landschaften gerendert. Im dritten Teil des Genesis-Projekts erfahren Sie, wie Sie Ihre Landschaft realistisch aussehen lassen und geschickt texturieren. Dazu verwenden Sie mehrere Texturierungs­schritte. Mit weiteren Algorithmen zur Sichtbar­keitsberechnung optimieren Sie die Renderge­schwindigkeit.

Shadow Map

SIE VERWENDEN den zweidimensionalen Emboss-Filter, um die Beleuchtung zu berechnen.
SIE VERWENDEN den zwei­dimensionalen Emboss-Filter, um die Beleuchtung zu berechnen.

In der letzten Ausgabe haben Sie die Landschaft schattiert, indem Sie eine Textur mit der Helligkeits­information (Fademap) über die ganze Landschaft gespannt haben. Diese Helligkeits­information hängt von der Beleuchtung der Landschaft durch eine Lichtquelle – in unserem Fall die Sonne – und von der Neigung der Landschaft zur Einfalls­richtung des Lichts ab.

Diese Helligkeits­informationen generieren Sie aus der Heightmap (vgl. Ausgabe 5/01, S. 246) mit einem Emboss Filter. Diesen definieren Sie mit einer Filtermatrix. Diese wenden Sie auf Ihr Bild an, indem Sie die Matrix wie eine Schablone über das Bild legen. Nun multiplizieren Sie die Pixelwerte mit den Zahlen in der Matrix. Die Summe dieser Wert ergibt die gewünschte Helligkeit in der Landschaft.

MIT RAYCASTING berechnen Sie die Schatten auf der Landschaft.
MIT RAYCASTING berechnen Sie die Schatten auf der Landschaft.

Auch Schatten verstärken den realistischen Eindruck. Auch diese können Sie aus der Heightmap berechnen. Betrachten Sie die Heightmap mit Ihren Höhen­informationen. Von jedem Pixel, dessen Höhe Sie kennen, schicken Sie einen Strahl zur Lichtquelle (Raycasting).

Wenn dieser Strahl einen Teil der Landschaft schneidet, liegt der zum Strahl gehörende Pixel der Heightmap im Schatten. In der Textur, die Sie mit dem Emboss Filter erzeugen, verdunkeln Sie die Pixel im Schatten. Diese beiden Schritte können Sie direkt nach der Generierung der Heightmap (siehe Beispiel­programm lsgen, 5/01) erledigen. Deshalb haben wir den Landschaft­generator aus der letzten Ausgabe um dieses Feature erweitert.

Texturierung ausgereizt

Es gibt zahlreiche Methoden, um Landschaften zu texturieren. Welche sie einsetzen sollten, hängt von der Zielplattform ab (welche Grafik-Hardware unterstützt werden soll), vom Speicherbedarf der Texturen, und davon, ob die Landschaft eher statisch oder dynamisch sein soll. Dynamisch sind sich ständig verändernde Landschaften, wie sie in vielen Computer­spielen vorkommen. Stellen Sie sich zum Beispiel eine Gegend vor, die Arbeiter einebnen, um dort besser bauen zu können.

WEIL DIE TEXTUREN ohne Rand aneinander passen, können Sie jede Kachel einzeln färben.
WEIL DIE TEXTUREN ohne Rand aneinander passen, können Sie jede Kachel einzeln färben.

Die unten aufgeführten Texturierungs­methoden arbeiten mit drei oder mehr Texturen, die Sie verknüpfen können. Bei einem Texturierungs­schritt spricht man von einem Renderpass. Neuere Grafikkarten haben zwei oder mehr Texture Units, mit denen Sie mehrere Texturen gleichzeitig rendern und verknüpfen können.

Im Quellcode lsrender finden Sie zu jeder Methode die Variante, die nur eine Texture Unit verwendet, und das Pendant dazu, das zwei Units auslastet. Der 3D-Beschleuniger verwendet immer den Befehl

glTexEnv[i/f](...)

Bisher haben Sie eine Textur mit der folgenden Option gerendert:


glTexEnvf(GL_TEXTURE_ENV,
	GL_TEXTURE_ENV_MODE, GL_MODULATE);
glDisable(GL_BLEND);
		

Die vordefinierte Konstante GL_MODULATE hat festgelegt, dass Sie jeweils die Farb- und Alphawerte des bereits gerenderten Bildes und die der aktuellen Textur multiplizieren. Im Beispiel­programm des letzten Teils wurde die Grundfarbe der Landschaft mit der Fademap multipliziert, wodurch der Beleuchtungs­effekt entstand. Wenn Sie die Schatten und Schattierung der Landschaft beibehalten wollen, benötigen Sie also einen Renderpass allein für diesen Effekt.

Bereichern Sie Landschaften mit Detailmaps. Diese Texturen enthalten zufällige Grauwerte. Detailmaps müssen seamless sein: Sie müssen sie nebeneinander legen können, ohne dass die Ränder sichtbar sind.

Die Detailmap im Bild oben Mitte wird nicht über die ganze Landschaft gestreckt, sondern sehr oft wiederholt. Sie ist also in viel höherer Auflösung zu sehen als die Fademap. Die Grauwerte der Detailmap verwenden Sie, um die Farbwerte abzudunkeln. Dazu überblenden Sie Texturen (Texture Blending) und zeichnen die Landschafts­polygone ein zweites Mal, nachdem Sie folgende Renderstates gesetzt haben:


glTexEnvf(GL_TEXTURE_ENV,
	GL_TEXTURE_ENV_MODE, GL_MODULATE);
glEnable(GL_BLEND);
glBlendFunc(GL_ZERO, GL_SRC_COLOR);
		

Für die Funktion glBlendFunc(...) bestimmen verschiedene Parameter, wie die Farbwerte verknüpft werden. Der erste Parameter bezieht sich auf das, was anschließend gerendert wird. Der zweite bestimmt, wie sich das Gerenderte auswirkt. Im obigen Beispiel multiplizieren Sie Farbwerte miteinander (GL_MODULATE) und übernehmen das Ergebnis (GL_SRC_COLOR). Die Farbwerte der Detailmap sind nur für die Multiplikation wichtig (GL_ZERO). Detailmaps beeindrucken mit einer viel höheren Textur­auflösung. Ihr Einsatz lohnt sich damit immer, wenn es um eine realistische Darstellung geht.

Diese Variante verwendet nur eine Texture Unit, so dass Sie alle Polygone doppelt zeichnen müssen. Damit Sie auf die Funktionen zugreifen können, die Sie für mehrere Units benötigen, müssen Sie die OpenGL Extensions importieren. Zuerst binden Sie den OpenGL-Extension-String ein, der alle Erweiterungen aufzählt, die Ihre Grafikkarte unterstützt:


char *extensions;
extensions = strdup((char*)glGetString(GL_EXTENSIONS));
for (int i = 0; i < strlen(extensions); i++)
	if(extensions[i] == ' ')
		extensions[i] = ‘\n’;
		

Wenn die beiden Schlüssel­wörter GL_ARB_multitexture und GL_EXT_ texture_env_combine, die mehrere Texture Units unterstützen, in diesem String enthalten sind, importieren Sie die Funktionen wie folgt:


// Konstanten Definitionen:

#include „glext.h“
PFNGLMULTITEXCOORD2FARBPROC
	glMultiTexCoord2fARB = NULL;
PFNGLACTIVETEXTUREARBPROC
	glActiveTextureARB = NULL;

if(strstr(extensions, "GL_ARB_multitexture") &&
	strstr(extensions, „GL_EXT_texture_env_combine“))
	{
		// anzahl der texture units:
		glGetIntegerv(GL_MAX_TEXTURE_UNITS_ARB,
			&maxTexelUnits);
		glMultiTexCoord2fARB = (PFNGLMULTITEXCOORD2FARBPROC)
			wglGetProcAddress("glMultiTexCoord2fARB");
		glActiveTextureARB = (PFNGLACTIVETEXTUREARBPROC)
			wglGetProcAddress("glActiveTextureARB");
...

}
		

Mit den neuen Funktionen können Sie zwei Texturen gleichzeitig wählen und jedem Vertex zwei Sätze von Textur­koordinaten im Immediate Mode zuweisen:


// texture unit #0 wählen
glActiveTextureARB(GL_TEXTURE0_ARB);
glEnable(GL_TEXTURE_2D);
fadeMap.select();
// texture unit #1 wählen
glActiveTextureARB(GL_TEXTURE1_ARB);
glEnable(GL_TEXTURE_2D);
detailMap.select();
// UV Koordinaten
glMultiTexCoord2fARB(GL_TEXTURE0_ARB, 0.0, 1.0);
glMultiTexCoord2fARB(GL_TEXTURE1_ARB, 0.5, 0.8);
// und Zeichnen...
		

Im Streaming Mode, den Sie in der letzten Ausgabe kennengelernt haben, setzen Sie die Pointer (Zeiger) auf die Textur­koordinaten-Streams:


glClientActiveTextureARB(GL_TEXTURE0_ARB);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glTexCoordPointer(2, GL_FLOAT,0,pTexCoordStream);
glClientActiveTextureARB(GL_TEXTURE1_ARB);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glTexCoordPointer(2, GL_FLOAT,0,pTexCoordStream2);
		

Die Online-Hilfe listet die Konstanten der Blending-Modi und Literatur zu OpenGL auf.

Techniken des Texturierens

Nachdem Sie das Handwerkzeug des Multitexturing erarbeitet haben, können Sie mit den folgenden Techniken Landschaften texturieren:
• Die einfachste: Spannen Sie eine sehr große Textur über die ganze Landschaft – ähnlich wie bei der Fademap. Wenn Sie die Landschafts-Polygone näher betrachten, sehen Sie sehr schnell, dass eine detailreiche Textur, die noch Wege oder Straßen abbilden soll, eine sehr hohe Auflösung benötigt. Diese kann von 1024 x 1024 Pixeln bis zu 8192 x 8192 Pixeln reichen.

EINE DETAILMAP beschert zusätzlichen Realismus.
EINE DETAILMAP beschert zusätzlichen Realismus.

Diese Methode hat einen hohen Speicherbedarf und ist daher für moderne Grafikkarten konzipiert, die Textur­kompression unterstützen. Selbst große Speicher sind mit 8192 x 8192 = 67 108 864 Pixeln schnell gefüllt. Eine solche große Textur können Sie in einem Bildbearbeitungs­programm anlegen. Für diese Technik würden Sie mit einem oder zwei Renderpasses auskommen, wenn Sie zusätzlich Detailmaps einsetzen wollen.
• Eine ältere, oft verwendete Methode arbeitet mit einem Satz kleinerer Texturen. Diese Texturen stellen jeweils einen Landschaftstyp dar.

Unterteilen Sie eine Landschaft in Felder. Im Beispiel­programm der letzten Ausgabe haben Sie aus der Heightmap Triangle-Strips generiert. Jeweils zwei Dreiecke ergeben ein Quadrat (Landschafts­feld). Weisen Sie jedem Feld eine Textur zu. Sie benötigen nicht nur Texturen für jeden Landschaftstyp, sondern auch für Übergänge, etwa von Sand- nach Felsboden. Damit vervielfacht sich die Anzahl der Texturen.

Für dieses Verfahren spricht der geringe Speicher­verbrauch. Obwohl Sie viele Texturen benötigen, sind diese relativ klein. Schon Texturen mit einer Auflösung von 32 x 32 bis zu 64 x 64 Pixeln ergeben beachtliche Landschaften. Dabei ergibt sich ein geschätzter Speicher­verbrauch von 300 x 64 x 64 = 1 228 800 Pixeln; das sind ungefähr 1,8 Prozent von dem der vorherigen Methode.
• Die dritte Variante benötigt für jeden Landschaftstyp nur eine Textur, mit der Sie die Landschafts­felder texturieren können. Diese Texturen müssen seamless sein.

Weisen Sie jedem Landschafts­feld zwei Landschafts­texturen zu. Eine weitere Textur spannen Sie über die ganze Landschaft. Hierfür genügt eine relativ niedrige Auflösung. Diese dritte Textur enthält die Information, wie die zwei voherigen Texturen überblenden. Diese Methode sehen Sie am Beispiel im Bild.

SIE ERKENNEN keine Grenzen in der Landschaftstexturierung, wenn Sie die Überblendtechnik verwenden.
SIE ERKENNEN keine Grenzen in der Landschaftstexturierung, wenn Sie die Überblendtechnik verwenden.

Sie können mit wenig Aufwand und wenig Texturspeicher sehr schöne Übergänge zwischen Landschafts­regionen erzeugen. Die im Bild angedeuteten Multi­plikations- und Additions­schritte erledigt die Grafik-Hardware. Setzen Sie die verschiedenen Texturen und die Texture Units so geschickt ein, dass Sie mit möglichst wenig Renderpasses auskommen. Das fängt schon bei der Organisation der Daten an. Nehmen wir an, Sie wollen die Fademap, die Blendmap und zwei Landschafts­texturen miteinander verknüpfen. Mit einem Bildbearbeitungs­programm basteln Sie eine 32-Bit-Textur, deren RGB- (Farb-) Kanäle die Fademap enthalten. In den Alpha-Kanal der Textur kopieren Sie die Blendmap für die Landschaft. Sie rendern dann wie folgt, wobei das Beispiel von einer Texture Unit ausgeht:


// 32 Bit Textur
blendMap.select();
glTexEnvf(GL_TEXTURE_ENV,
	GL_TEXTURE_ENV_MODE, GL_MODULATE);
glDisable(GL_BLEND);
renderStream(pTexCoordStream);

// erster Landschaftstyp
basisMap1.select();
glEnable(GL_BLEND);
glBlendFunc(GL_DST_ALPHA, GL_SRC_COLOR);
renderStream(pTexCoordStream2);

// zweiter Landschaftstyp
basisMap2.select();
glBlendFunc(GL_ONE_MINUS_DST_ALPHA,
	GL_DST_COLOR);
renderStream(pTexCoordStream2);
		

Wolken am Himmel

Mit diesen Rendering- und Texturierungs-Tricks lassen sich Landschaften sehr realistisch darstellen. Um den noch fehlenden Himmel darzustellen, können Sie eine sehr große Halbkugel wie eine Glocke über Ihre Landschaft platzieren. Dieser Halbkugel verpassen Sie eine Textur, auf der Wolken und/oder Sonne zu sehen sind (Skydomes). Statt einer Halbkugel können Sie auch einen Zylinder verwenden, wenn der Kamera­blickwinkel so eingeschränkt ist, dass der Betrachter nicht sehr steil nach oben sehen kann.

Für diese beiden Varianten lassen sich Texturen mit Fotos, Bildbearbeitungs­programmen oder dem Midpoint Displacement Algorithmus (Heft 5/01, S.247, siehe gleichlautende Zwischen­überschrift).

Noch eleganter sind Skyboxes. Die Theorie dahinter: Ein Betrachter befindet sich an einem festen Punkt. Von diesem Punkt aus machen Sie sechs Fotos mit 90 Grad Öffnungswinkel in jeweils beide Richtungen des 3D-Koordinaten­systems. Wenn Sie diese Fotos als Texturen auf einen Würfel kleben und die Kamera in der Mitte des Würfels platzieren, können Sie in jede Richtung blicken und werden stets eine korrekte Perspektive haben.

Da der Betrachter bei unserer Landschafts­darstellung nicht an einer Stelle stehen bleibt, stimmt die Theorie nicht mehr ganz. Sie trifft aber für sehr weit entfernte Objekte wie Sonne und Wolken zu.

Entsprechende Texturen zu erzeugen, ist kompliziert, da Sie eine Verzerrung an den Ecken berück­sichtigen müssen. Benutzen Sie das Zeichen­programm Skypaint, das Sie unter www.skypaint.com laden können. Um fertige Skybox-Texturen zu genieren, nutzen Sie das kommerzielle Programm Bryce 3D.

Atmosphärische Effekte

Um in einer 3D-Anwendung atmosphärische Effekte in Echtzeit darzustellen, nutzen Sie das so genannte Fogging. Dabei werden die Farbwerte beim Rendering abhängig von ihrer Entfernung zum Betrachter mit einer vorher festgelegten Farbe gemischt und können leicht Nebeleffekte erzeugen.

MIT FOGGING modellieren Sie atmosphärische Effekte.
MIT FOGGING modellieren Sie atmosphärische Effekte.

Um diesen Effekt zu erreichen, fügen Sie folgende Codezeilen in Ihr Programm ein:


glEnable(GL_FOG);
glFogi(GL_FOG_MODE, GL_EXP2);
glFogf(GL_FOG_DENSITY, 0.01f);
GLfloat fogColor[3] = { 1.0f, 1.0f, 1.0f };
glFogfv(GL_FOG_COLOR,fogColor);
		

Renderspeed

Wenn Sie Ihre Grafikkarte mit den Daten der bisher vorgestellten Rendertricks belasten, kann es zu einer Performance-Krise kommen. Immerhin haben Sie es mit bis zu 256 x 256 x 2 = 131 072 Dreiecken bei bis zu drei Renderpasses zu tun, also 393 216 gezeichneten Dreiecken. Es gilt daher, mit einem einfachen Algorithmus wirkungsvoll zu intervenieren. Trotz optimierter Daten­strukturen ist es sinnvoll, eine gewisse Vorauswahl zu treffen, welche Teile der Landschaft sichtbar sein können.

Zuerst sollten Sie die Landschaft unterteilen. Damit sich die Triangle-Strips noch rentieren, sollten diese Teile nicht zu klein sein. Erfahrungs­werte optimieren Sie mit Experimenten. Es hat sich bewährt, die Landschaft mit 256 x 256 Feldern in 16 x 16 Sektoren zu 16 x 16 x 2 Dreiecke zu unterteilen. Für jeden dieser Sektoren berechnen Sie eine Bounding Box: ein möglichst kleiner Quader, der alle Dreiecke des Sektors enthält.

Am einfachsten lassen sich Axis Aligned Bounding Boxes berechnen. Dabei handelt es sich um Quader, deren Kanten parallel zu den Koordinaten­achsen verlaufen. Die Eckpunkte der Quaders erhalten Sie, indem Sie die minimalen und maximalen x-, y- und z Koordinaten aller Vertizes eines Sektors bestimmen und darauf die Eckpunkte konstruieren. Den Sourcecode dazu finden Sie in clipper.h auf der Heft-CD.

IN UNSEREM LANDSCHAFTSRENDERER steuern Sie mit den Cursortasten die Kamera.
IN UNSEREM LANDSCHAFTSRENDERER steuern Sie mit den Cursortasten die Kamera.

Der von der Kamera sichtbare Bereich ist ein Pyramiden­stumpf im Raum (der so genannte Viewing Frustum), den sechs Begrenzungs­ebenen einschließen. Wenn die Bounding Box diesen Viewing Frustum nicht schneidet, sind die Dreiecke des zugehörigen Landschafts­sektors nicht sichtbar. Somit können Sie eine Vielzahl von Dreiecken vom Rendering ausschließen, die nicht zur Grafikkarte geschickt werden müssen.

Doch wie bekommen Sie die Information über den Viewing Frustum, und wie stellen Sie fest, ob eine Bounding Box diesen schneidet? Glücklicher­weise lässt sich der Viewing Frustum aus der Transformation, die ein Vertex durch die Modelview- und die Projektions­matrix erfährt, rekonstruieren. Die entsprechende Routine buildFrustum(), die Ihnen die Ebenen­gleichungen der Begrenzungs­ebenen berechnet, finden Sie auch in clipper.h.

Per Definition der Ebenen­gleichung teilt eine Ebene den Raum in zwei Hälften: In eine zeigt die Normale, die andere Hälfte ist die Entgegen­gesetzte. Mit dem Skalarprodukt können Sie feststellen, auf welcher Seite sich ein Vertex befindet. Damit können Sie auch berechnen, ob eine Bounding Box das Viewing Frustum schneidet, komplett umfasst oder vollständig außerhalb liegt. Nur in den ersten beiden Fällen müssen die Dreiecke des Sektors gerendert werden. Je nach Kameraposition und Blickwinkel lassen sich damit bis zu 98 Prozent der Dreiecke von vornherein ausschließen. Wenn Sie eine Panorama-Ansicht der Landschaft genießen wollen, werden Sie mit diesem Algorithmus nicht viel Einsparung feststellen. Aber bei geläufigen Ansichten wie in Computer­spielen ist die Einsparung enorm.