Tutorial Cubemap

Aus DGL Wiki
Version vom 22. Dezember 2005, 14:53 Uhr von La Boda (Diskussion | Beiträge)

(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche

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 Reflektionen 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-Reflektion (kommt in Bewegung natürlich noch besser daher) :

Tutorial Cubemap cubemap0.jpg Tutorial Cubemap cubemap1.jpg Tutorial Cubemap cubemap2.jpg


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 :

Tutorial Cubemap cm right.jpg Tutorial Cubemap cm left.jpg Tutorial Cubemap cm top.jpg Tutorial Cubemap cm bottom.jpg Tutorial Cubemap cm front.jpg Tutorial Cubemap cm back.jpg
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. Ausserdem 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. Ausserdem 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 Reflektionscubemap via Multitexturing recht schwach auf ein Objekt und da reichen dann auch 128x128 Pixel große Texturen.


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 projezieren 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-Reflektion 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 :

Tutorial Cubemap cubemap 0.jpg Tutorial Cubemap cubemap 1.jpg Tutorial Cubemap cubemap 2.jpg Tutorial Cubemap cubemap 3.jpg Tutorial Cubemap cubemap 4.jpg Tutorial Cubemap cubemap 5.jpg

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.

Tutorial Cubemap normcubemap.jpg


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 Refkletkionsmethode eingeht (allerdings in Englisch), sollte sich mal dieses ausführliche Tutorial von Nvidia ansehen.

Euer Sascha Willems(webmaster_at_delphigl.de)