Tutorial Cubemap
Inhaltsverzeichnis
GL_ARB_Texture_Cube_Map
Ave!
Und willkommen zu Tutorial Nummer drei in meiner Extension-"Reihe". Heute beschäftigen wir uns mit der GL_ARB_Texture_Cube_Map-Exension (Spezifikation), welche v.a. zur Realisierung von Reflexionen genutzt werden kann (und wird). Beim Cubemapping gibt man nicht wie bisher nur eine einzige Textur als Ziel an, sondern wie der Name vermuten lässt sechs Texturen, die dann einen Würfel (deshalb Cubemapping) darstellen. Und anstatt dann wie beim normalen Texturzugriff über GL_TEXTURE_*D die Texturkoordinaten zur Positionsermittlung eines Texels zu nutzen, werden dann die Koordinaten s,t und r als Richtungsvektor genutzt, über den dann ermittelt wird welches Texel aus welcher Würfeltextur ausgelesen wird.
Ich geb zu das dies erstmal recht komplex klingt, allerdings wurde mit dieser Extension auch ein neuer Modus zur automatischen Generierung von Cubemap-Texturkoordinaten entworfen, so das die Nutzung von Cubemapping sehr einfach von der Hand gehen sollte.
Um euch etwas anzuspornen hier drei Screenshots des berühmten Teekessels mit aktiver Cubemap-Reflexion (kommt in Bewegung natürlich noch besser daher) :
Hardwareunterstützung
Hardwareseitig wird Cubemapping bereits seit Nvidias GeForce 1 unterstützt und selbst der original RivaTNT wurde dieses Feature über eine treiberseitige Softwareimplementation beigebracht. Von daher kann man diese GL-Funktionalität ruhigen Gewissens als vorhanden ansehen, sofern man sich nicht mit einer Ur-Alt Grafikkarte abgeben will. Wer sich trotzdem mal ein Bild über die Hardwareunterstützung machen will, sollte in diesem Eintragvon Tom Nuydens Seite fündig werden.
Laden der Cubemap
Wie anfangs erwähnt besteht die Cubemap aus insgesamt sechs Seiten, die eine Panoramaansicht (=360°) der zu reflektierenden Umgebung darstellen. Erstellen lässt sich sowas eigentlich recht leicht in einem 3D-Modeller, indem man die Szene jeweils um 90° rotiert mit einem FOV von 90° rendert (und dann noch nen Deckel und den Boden). Für dieses Tutorial hab ich mich allerdings der im Nvidia-SDK mitgelieferten Cubemaptexturen bedient. Prinzipiell könnt ihr übrigens auch jede Skybox als Cubemap nutzen, ihr müsst dann aber die ein oder andere Seite erst richtig spiegeln bzw. drehen bevor ihr sie hochladen könnt. Da die Texturen jedoch als Cubemap und nicht als einfache 2D-Texturen geladen werden müssen, wurden mit dieser Extension auch neue Texturenziele deklariert, welche die einzelnen Seiten des Würfels repräsentieren. Alle Ziele beginnen mit GL_TEXTURE_CUBE_MAP_, gefolgt von ihrer absoluten Position. Zum leichteren Verständnis präsentiere ich euch diese neuen Ziele (ohne das obigen Präfix) direkt zusammen mit den passenden Texturen, was die Sache verständlicher machen sollte :
POSITIVE_X_ARB (Rechts) | NEGATIVE_X_ARB (Links) | POSITIVE_Y_ARB (Oben) |
NEGATIVE_Y_ARB (Unten) | POSITIVE_Z_ARB (Vorne) | NEGATIVE_Z_ARB (Hinten) |
Anhand obiger Bezeichnungen sollte leicht erkennbar sein, welche der Konstanten für welche Seite des Würfels steht. Wenn ihr einen Blick in den OpenGL-Header werft, werdet ihr übrigens sehen dass das die Werte für diese Konstanten alle nacheinander folgende Zahlenwerte, beginnend bei $8515 darstellen. Das Tolle an dieser Tatsache ist nun, dass wir so unsere Cubemaptexturen ganz leicht in einer Schleife laden können und die Texturnamen in einem einfachen Stringarray übergeben können:
const
CubeMapFileName : array[0..5] of String = ('cm_right.tga','cm_left.tga','cm_top.tga',
'cm_bottom.tga','cm_front.tga','cm_back.tga');
for i := 0 to 5 do
begin
Target := GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB+i;
LoadTexture(CubeMapFileName[i],Cubemap[i], False, GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, False);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
end;
Dieser Quellcode sollte recht einfach zu begreifen sein, bedarf aber evtl. noch einer kleinen Erklärung. Am Anfang des Schleifenkörpers setze ich die Variable Target auf die zu ladende Seite des Cubemap-Würfels. Diese Variable habe ich der textures.pas von Jan Horn hinzugefügt, da diese in ihrer Originalform nur das Laden von 2D-Texturen ermöglicht. Ihr müsst eurem Texturenlader (egal welchen ihr nutzt) also mitteilen das ihr die Seiten der Cubemap und keine herkömmliche Textur laden wollt. Außerdem setze ich dann noch den Clamp-To-Edge-Modus für alle Texturen, der wie u.a. auch bei einer Skybox dafür sorgt das an den Übergängen keine Kanten entstehen.
Kleiner Hinweis : Die Cubemaptexturen müssen übrigens alle in der gleichen Größe vorliegen. Außerdem sollte man es mit der Größe nicht übertreiben, zumal man in Spielen eher selten ein Objekt hat das nur reflektiert. Meistens legt man da die Reflexionscubemap via Multitexturing recht schwach auf ein Objekt und da reichen dann auch 128x128 Pixel große Texturen.
Alternativ kann man die Texturen auch komfortabel mit dem glBitmap-Loader laden |
Automatische Texturkoordinaten-Generierung
Einige von euch haben sich vielleicht schonmal die Möglichkeit der automatischen Generierung von Texutrenkoordinaten durch OpenGL zu Nutze gemacht, sei dies weil man sein Terrain schnell texturieren wollte oder weil man etwas auf seine Szene projizieren wollte. Genau diese Möglichkeit werden wir auch nutzen um unsere Refkletions-Cubemap-Texturkoordinaten von OpenGL generieren zu lassen. Zu diesem Zweck bringt uns die GL_ARB_Texture_Cube_Map-Extension einen neuen TexGenModus, nämlich GL_Reflection_Map_ARB. Diesen übergeben wir dann für die s,t und r-Koordinate, da beim Cubemapping wie eingangs besprochen ein Richtungsvektor zur Texelermittlung dient, der ja aus drei Komponenten besteht:
glTexGenf(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP_ARB);
glTexGenf(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP_ARB);
glTexGenf(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP_ARB);
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
glEnable(GL_TEXTURE_CUBE_MAP_ARB);
In der letzten Zeile aktivieren wir übrigens noch das Cubemapping. Würden wir das nicht tun,dann würden wir logischerweise auch nichts von unserer Cubemap sehen.
Rendern
Da ihr nun alles nötige getan habt, um die Cubemaptexturen hochzuladen und zu aktiveren, müsst ihr beim Rendern eures Objektes nichts Weiteres mehr machen, denn OpenGL kümmert sich nun dank der automatischen Generierung der Texturkoordinaten um das Anwenden der Cubemap. Damit wäre das Theme statische Cubemap-Reflexion auch schon abgeschlossen, und wenn ihr nicht mehr wissen wolltet, dann könnt ihr versuchen dieses Feature mal einzubinden. Alle anderen können gerne noch weiterlesen und sich an den zwei etwas fortgeschritteneren Kapiteln erfreuen.
Dynamische Cubemaps
Euch reicht eine statische Cubemap nicht aus und ihr wollt das sich eure Umgebung in Echtzeit spiegelt? Auch das ist natürlich mit Cubemapping kein Problem. Da der technische Aspekt dahinter aber eher in Richtung RenderToTexture geht, erkläre ich nur kurz das Prinzip dahinter, zumal die Implementation recht einfach sein sollte. Um eure Szene dynamisch über die Cubemap zu reflektieren müsst ihr "einfach" nur alle sechs Seiten des Würfels Frame für Frame in die entsprechenden Texturen rendern, dürft dabei allerdings das Objekt das reflektieren soll nicht mitzeichnen. Am besten geht das natürlich in einer einfachen Schleife und indem ihr über ein Array jeweils die Rotation für das aktuelle Cubemap-Face angebt. Nicht vergessen dürft ihr dabei das FOV von genau 90° um einen kompletten Rundumblick zu erhalten. Natürlich solltet ihr es auch mit diesem Feature nicht übertreiben, da das Rendern in eine Textur recht leistungshungrig ist und deshalb stark an der Framerate zerren kann. Daher bietet es sich z.B. an die dynamische Cubemap nur alle X-Frames zu aktualisieren.
Normalisierungscubemaps
Eine weiter oft genutzte Möglichkeit zur Verwendung einer Cubemap sind Normalisierungs-Cubemaps. Wie der Namen evtl. bereits vermuten lässt wird die Cubemap hier nicht genutzt um eine Szene zu reflektieren, sondern um Vektoren (also Farbewerte) zu speichern. So enthält jeder Texel der Cubemap dann einen Vektor, über den wir die Normale eines jeden Vektors unseres Objektes anhand des Richtungsvektors (durch die Texturkoordinaten s, t und r angegeben) ermitteln können. Dadurch ist es auch auf Hardware, die keine PixelShader bietet, möglich, Per-Pixel-Normalen zu nutzen.
Die Methode zur Erstellung einer solchen Cubemap ist recht einfach, denn in jedem Texel wird der Vektor zwischen Texelposition und Würfelmitte gespeichert, was dazu führt, dass die Normalisierungscubemap eigentlich immer gleich aussieht. Wer keine Lust hat dazu selbst ne Routine zu schreiben, der kann sich an meinem folgendem Quellcode bedienen :
procedure GenerateNormalisationCubeMap;
const
Offset = 0.5;
Size = 32;
HalfSize = Size div 2;
var
Data : array[0..Size-1,0..Size-1,0..2] of Byte;
TempVector : TglVertex3f;
i,j,CFace : Integer;
begin
for CFace := 0 to 5 do
begin
for i := 0 to High(Data) do
for j := 0 to High(Data[i]) do
begin
// Vektor vom Zentrum des Würfels zum aktuellen Texel berechnen
case GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB+CFace of
GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB : begin
TempVector.x := HalfSize;
TempVector.y := -(j+Offset-HalfSize);
TempVector.z := -(i+Offset-HalfSize);
end;
GL_TEXTURE_CUBE_MAP_NEGATIVE_X_ARB : begin
TempVector.x := -HalfSize;
TempVector.y := -(j+Offset-HalfSize);
TempVector.z := i+Offset-HalfSize;
end;
GL_TEXTURE_CUBE_MAP_POSITIVE_Y_ARB : begin
TempVector.x := i+Offset-HalfSize;
TempVector.y := HalfSize;
TempVector.z := j+Offset-HalfSize;
end;
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_ARB : begin
TempVector.x := i+Offset-HalfSize;
TempVector.y := -HalfSize;
TempVector.z := -(j+Offset-HalfSize);
end;
GL_TEXTURE_CUBE_MAP_POSITIVE_Z_ARB : begin
TempVector.x := i+Offset-HalfSize;
TempVector.y := -(j+Offset-HalfSize);
TempVector.z := HalfSize;
end;
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_ARB : begin
TempVector.x := -(i+Offset-HalfSize);
TempVector.y := -(j+Offset-HalfSize);
TempVector.z := -HalfSize;
end;
end;
// Vektor normalisieren
glNormalizeVector(TempVector);
// In 0..1-Reichweite packen
TempVector := glScaleVector(TempVector,glVertex(0.5,0.5,0.5));
TempVector := glAddVector(TempVector,glVertex(0.5,0.5,0.5));
// In Cubemap-Face schreiben
Data[j,i,0] := Round(TempVector.x*255);
Data[j,i,1] := Round(TempVector.y*255);
Data[j,i,2] := Round(TempVector.z*255);
end;
// Cubemap-Face an OpenGL senden
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB+CFace, 0, GL_RGBA8, Size, Size,
0, GL_RGB, GL_UNSIGNED_BYTE, @Data);
end;
end;
Der Code dürfte eigentlich keine Verständnisprobleme machen und liefert euch dann als Ergebnis (im VRAM der Grafikkarte) die sechs Seiten der Normalisierungscubemap. Die Größe habe ich auf 32x32 Pixel festgelegt, allerdings ändert sich bei einer Vergrößerung der Texturen rein gar nichts. Für alle die Probleme haben obigen Code zu visualisieren oder sehen wollen wie die Cubemap-Seiten aussehen :
Auf unseren beliebten Teekessel angewendet, sieht das dann folgendermaßen aus :
Wie gut zu erkennen haben wir jetzt quasi für jeden Pixel eine eigene Normale aus der Normalisierungscubemap, und dies ist wohlgemerkt auch auf Grafikkarten ohne Pixelshader möglich.
Nachwort
So viel also zum Thema Cubemap und Verwendungszwecke. Ich hab versucht in diesem Tutorial alle häufigen Nutzungsmöglichkeiten abzudecken, was allerdings noch fehlt ist die Nutzung einer Cubemap zu Darstellung von Schatten. Da dieses Thema allerdings sehr komplex ist, habe ich es bewusst nicht reingenommen. Hoffe also das Tut war mal wieder lehrreich und auch die Zeit wert!
Wer übrigens eine tiefergehende Beschreibung zu diesem Thema sucht, die auch auf andere Reflexionsmethode eingeht (allerdings in Englisch), sollte sich mal dieses ausführliche Tutorial von Nvidia ansehen.
Euer Sascha Willems(webmaster_at_delphigl.de)
|
||
Vorhergehendes Tutorial: - |
Nächstes Tutorial: Tutorial_Vertexbufferobject |
|
Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com. Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen. |