Tutorial Pixelbuffer

Aus DGL Wiki
Wechseln zu: Navigation, Suche

PixelBuffer (WGL_Pixel_Buffer_ARB)

Einleitung

Immer häufiger kommt es vor, dass die eigene GL-Anwendung in einen nicht sichtbaren Bereich rendern muss. Sei dies nun, weil man diesen unsichtbaren Bereich als eine Textur verwenden will (z. B. für Spiegel, Portale oder Kameras) oder damit Post-Scene-Effekte nutzen möchte (dann meist mit Shadern). OpenGL an sich hatte bisher allerdings keine Möglichkeit dies zu bewerkstelligen und man musste den Inhalt des hinteren Puffers früher mit gl(Sub)CopyTexImage in ein vorher erstelltes Texturenziel kopieren. Die Nachteile dieser Methode liegen auf der Hand: Der kopierbare Bereich ist maximal so groß wie die GL-Zeichenfläche (meist jedoch kleiner, da man Texturen im Normalfall nur in der Größenordnung 2^n * 2^n erstellen kann), laut GL-Spezifikation ist der Inhalt verdeckter Bereiche des GL-Kontextes (z. B. durch ein Dialogfenster) undefiniert, was dazu führen kann das man in solchen Situationen fehlerhafte Texturen bekommt, und zu guter Letzt teilt man sich so oft für verschiedene Renderansichten (meist rendert man in eine Textur ja was komplett anderes als sichtbar ist) den gleichen Kontext. Das macht die Verwaltung der OpenGL-States dann mit steigender Komplexität der Anwendung recht aufwändig.

Um diesem Dilemma ein Ende zu setzen, wurde bereits im März 2000 eine neue Extension ins Leben gerufen, die das Rendern in einen unsichtbaren Bereich ermöglichen sollte. Herausgekommen ist dabei WGL_Pixel_Buffer_ARB, wie zu erkennen eine windowsspezifische Extension. "Warum OS-abhängig?", wird sich der ein oder andere jetzt bestimmt fragen. Nun, das liegt ganz einfach daran, dass jeder Pixelpuffer (der im Endeffekt nicht sehr viel mehr als ein Teil des Grafikspeichers darstellt) auch ein eigenes Pixelformat mitbringt. Das scheint zwar in einfachen Situationen übertrieben, aber man wird recht schnell merken, dass dies gar keine schlechte Idee war. Und schlussendlich besitzt jeder Pbuffer auch noch seinen eigenen Renderkontext, wodurch man dort unabhängig vom Hauptkontext OpenGL-States ändern kann, ohne das diese später angetastet werden. Einen Nachteil hat das natürlich: Kontextwechsel sind recht teuer, wer also viele Pbuffer nutzen muss wird das an der Performance seiner Anwendung sehen.

Erweiterungen

Folgende Extensions gehören zum Bereich Pixelpuffer, wobei nicht alle unterstützt werden müssen und einige davon dazu dienen die Funktionalität des Pixelpuffers zu erweitern:

  • WGL_ARB_pbuffer
  • WGL_ARB_pixel_format
  • WGL_ARB_make_current_read
  • WGL_ARB_render_texture

Kurz zum Tutorial

Damit ihr am Ende dieses Tutorials auch einen praktischen Nutzen habt, werden wir hier zusammen im Verlaufe des Textes eine kleine (und sicherlich erweiterbare) Klasse bauen, die uns den Pixelpuffer kapselt. Denn v. a. Dessen Erstellung ist recht aufwändig, was also geradezu nach einer entsprechenden Klasse schreit.

Die Klasse TPixelBuffer

Unsere Pixelpuffer-Klasse kapselt also wie gesagt alle Vorgänge und Variablen, die man zur Erstellung und Verwaltung eines Pixelpuffers benötigt:

TPixelBuffer = class(TComponent)
 public
  Log       : TstringList;
  DC        : HDC;
  RC        : HGLRC;
  ParentDC  : HDC;
  ParentRC  : HGLRC;
  Handle    : glUInt;
  Width     : glUInt;
  Height    : glUInt;
  TextureID : glUInt;
  function IsLost : Boolean;
  procedure Enable;
  procedure Disable;
  procedure Bind;
  procedure Release;
  constructor Create(...); reintroduce;
  destructor Destroy; override;
end;

Wir leiten unsere Klasse idealerweise von TComponent ab. Für alle die sich nicht mit den Klassengrundlagen beschäftigt habe : TComponent hat den Vorteil das man ihm im Konstruktor das besitzende Element angeben kann, und sobald der Besitzer aus dem Speicher entfernt wird, wird auch die damit verbundene Klasse aus dem Speicher entfernt (sprich: der Destruktor aufgerufen). Wenn wir unseren Pixelpuffer später erstellen, übergeben wir ihm also als Besitzer unsere Form und müssen dann nicht explizit bei der Beendigung des Programms den Desktrukor des Puffers aufrufen, das macht Delphi dann für uns.

Wie zu sehen bringt die Klasse alle wichtigen Eigenschaften eines Pixelpuffers mit sich. Sowohl den Renderkontext, als auch den Gerätekontext. Außerdem noch als Eigenschaften vorhanden sind der übergeordnete (sprich: der Hauptanwendung) DC und RC, damit wir im Pixelbuffer wieder zum Renderkontext der eigentliche Anwendung zurückschalten können. Nebenbei gibt es auch noch eine TStringList, die uns als Log dient. So können Fehler recht schnell gefunden werden, und dazu noch separat für jeden Puffer.

Vorarbeit

Bevor wie den unsichtbaren Puffer zum Rendern nutzen können, müssen wir diesen natürlich zuerst erstellen. Dies geschieht übrigens in einer ähnlichen Form wie dies mit einem normalen Kontext geschieht. Man benötigt also einen Gerätekontext, sowie einen Renderkontext (spätestens jetzt sollte jedem dämmern warum der Pbuffer denn OS-spezifisch ist); und natürlich die neu eingeführten Funktionen, um diesen zu Erstellen. Die Funktionszeiger sind zumindest in unserem hauseigenen Header DGLOpenGL.pas enthalten, also empfehle ich dessen Nutzung.

Bevor wir jedoch zur eigentlichen Erstellung des Puffers kommen, müssen wir uns erstmal Gedanken über dessen Eigenschaften machen, die je nach Nutzungszweck des Puffers (z.B. für Fließkommatexturen) anders ausfallen. Die Eigenschaften werden dazu in einem Array abgelegt, das in folgender Form aufgebaut sein muss:

[Eigenschaft][Wert der Eigenschaft]...[0]

Es ist wichtig das dieser Aufbau eingehalten wird, denn das Vergessen einer Werteigenschaft oder der abschließenden Null führt dazu, dass der Pixelpuffer nicht erstellt werden kann. Von diesen Eigenschaftslisten benötigen wir zwei an der Zahl. Die erste (im Regelfall längere) Liste beschreibt das Pixelformat für unseren Pixelpuffer, während die zweite den Pixelpuffer beschreibt.

Eigenschaften des Pixelformates

Bevor wir uns nun ans Eingemachte machen, erkläre ich kurz zumindest die wichtigsten Eigenschaften, die das Pixelformat des PPuffers besitzen kann:

WGL_SUPPORT_OPENGL_ARB

Da unser Puffer OpenGL unterstützen soll (was anderes macht in unserem Falle keinen Sinn), müssen wir dies in der Attributsliste eigentlich auf GL_TRUE (INT=1) setzen. Allerdings werden sowohl auf NVidia- als auch auf ATI-Hardware nur Pixelformate mit OpenGL-Unterstützung zurückgeliefert. Allerdings muss das bei anderen Implementationen nicht der Fall sein, also sicherheitshalber kann die Angabe dieser Eigenschaft nicht schaden.

WGL_COLOR_BITS_ARB

Gibt die Anzahl der Bits des Farbpuffers an (insgesamt, nicht pro Kanal), exklusive Alpha.

WGL_ALPHA_BITS_ARB

Anzahl der Bits für den Alphakanal. Im Normalfall entweder 0 für keinen Alpha oder 8.

WGL_DEPTH_BITS_ARB

Bitzahl des Tiefenpuffers.

WGL_DOUBLE_BUFFER_ARB

Gibt an ob unser Pixelpuffer doppelte Pufferung unterstützen soll. Für den Renderkontext der Anwendung kommt man ohne Doppelpuffer natürlich nicht aus, aber einen Pixelpuffer mit Doppelpuffer benötigt man nur sehr selten, da dieser normalerweise im Verborgenen liegt und nicht direkt dargestellt wird. Hier sollte man also GL_FALSE wählen um so Speicher zu sparen.

WGL_DRAW_TO_PBUFFER_ARB

Das ist natürlich auch eine unbedingt nötige (=GL_TRUE) Eigenschaft, mit der wir ein Pixelformat anfragen, das in einen Pixelpuffer rendern kann.

WGL_ACCELERATION_ARB

Eine Eigenschaft, die man öfter sieht, die allerdings im Normalfall weggelassen werden kann. Eigentlich soll hier ein GL_TRUE angeben das nur Pixelformate zurückgeliefert werden, die hardwarebeschleunigt sind, aber zumindest auf NVidia und ATI-Karten werden sowieso nur eben solche Formate zurückgeliefert, weshalb man sich diese Eigenschaft sparen kann (es sei denn man will auf Nummer Sicher gehen).

Das sollte für den Anfang (also den grundlegenden Pixelpuffer) reichen, und alle anderen Eigenschaften des Pixelformates (die man eher selten benötigt) können dann in den Specs (siehe Kapitel "Erweiterungen") nachgeschlagen werden.

Quellcodetechnisch sieht eine solche Liste dann so aus (deklariert in der Konstantensektion):

PixelFormatAttribs : array[0..12] of GLUInt =
(WGL_SUPPORT_OPENGL_ARB, GL_TRUE,
WGL_DRAW_TO_PBUFFER_ARB, GL_TRUE,
WGL_COLOR_BITS_ARB, 24,
WGL_ALPHA_BITS_ARB, 8,
WGL_DEPTH_BITS_ARB, 24,
WGL_DOUBLE_BUFFER_ARB, GL_FALSE, 0);

Die Suche nach dem passenden Pixelformat

Nach dem ganzen theoretischen Kram da oben kann es nun endlich losgehen, und zwar mit der Suche nach dem für unseren Pixelpuffer benötigtem Pixelformat. Ähnlich wie beim Pixelformat für unsere Hauptanwendung (alle dies nicht selber machen, sollten mal einen Blick in den Quellcode der Funktion CreateRenderingContext in unserem Header werfen) verfügt die Hardware über eine (recht große) Liste an möglichen Pixelformaten (mit verschiedenen Eigenschaften, also z. B. für alle Farbtiefen) aus der man das passenden heraussuchen muss. Wenn keines gefunden wird, dann schlägt natürlich die Erstellung des Pixelpuffers fehl.

Zum Glück gibt es bereits eine fertige Funktion namens wglChoosePixelFormatARB die uns ein passendes Pixelformat suchen soll. Hier kurz die Parameter sowie deren Bedeutung/Funktion in unserem Falle:

wglChoosePixelFormatARB : BOOL
Die Funktion liefert uns also TRUE zurück wenn mindestens ein passendes Pixelformat gefunden wurde. Ansonsten FALSE, was bedeutet das wir mit diesem Pixelformat keinen Pixelpuffer erstellen können und wir jetzt den Versuch beenden müssen (passender Eintrag in den Log schreiben und dann per exit raus aus der Funktion).
hdc: HDC
Ein gültiger Gerätekontext. Am bequemsten kommt man da über die Funktion wglGetCurrentDCran, die uns den aktuell aktiven Gerätekontext zurück gibt.
piAttribIList: PGLint
Ein Zeiger auf unsere oben beschriebene Attributsliste. Das große „I“ gibt an das es sich um eine Liste handelt in der sich nur Integerwerte befinden.
pfAttribFList: PGLfloat
Zeiger auf eine Liste mit Attributen im Fließkommaformat. Dies wird selten genutzt und von uns hier nicht benötigt. Deshalb übergeben wir hier einen Zeiger auf einen mit null vorinitialisierten Fließkommawert.
nMaxFormats: TGLuint
Gibt an wie viele Pixelformate wir maximal zurückgeliefert bekommen wollen. Da unser Format recht genau beschrieben wurde reicht hier ein vergleichsweise kleiner Wert, denn man schlauer weise mit der Länge des Arrays das als nächster Parameter übergeben wird gleichsetzen sollte, was die Sache dynamischer macht.
piFormats: PGLint
Wie eben erwähnt werden hier alle in Frage kommenden (sprich mit den Eigenschaften die wir benötigen) Pixelformate abgelegt, und zwar deren Indices. Also müssen wir hier einen Zeiger auf ein Integerarray übergeben, das logischerweise mindestens nMaxFormats groß sein muss.
nNumFormats: PGLuint
Letztendlich bekommen wir dann noch (als Zeiger auf einen Integer) die Zahl der gefundenen (und damit auf unsere Anfrage passenden) Pixelformate zurückgeliefert.

Wenn wir dies nun geschafft haben und ein entsprechendes Pixelformat gefunden haben, ist der Großteil der Arbeit auch schon erledigt. Als Anhang auch noch der passende Quellcode:

const
 EmptyF : TGLFLoat = 0;
var
 PFormat    : array[0..64] of TGLUInt;
 NumPFormat : TGLUInt;
 TempDC     : TGLUInt;
...
TempDC := wglGetCurrentDC;
...
if wglChoosePixelFormatARB(TempDC, @PixelFormatAttribs, @EmptyF, Length(PFormat), @PFormat, @NumPFormat) then
...

Den Pixelpuffer erstellen

Nach langem hin und her sind wir endlich am Kernpunkt angelangt und wollen nun unseren Pixelpuffer erstellen. Dazu gibt es die Funktion wglCreatePBufferARB, die neben dem Gerätekontext, dem Pixelformat (im Normalfall sollte man das erste gefundene Pixelformat übergeben, also das mit dem Index 0) und den Dimensionen des Pixelpuffers (Achtung! Das muss wie gewohnt 2^n*2^n sein, solange man nicht eine Extension verwendet die NPOT-Texturen nutzbar macht, was momentan in den seltensten Fällen so sein wird) auch noch eine Attributsliste erhält.

Diese Liste ist im Regelfall recht kurz und beschreibt kurz, was genau wir mit unserem Pixelpuffer anfangen wollen; der Aufbau entspricht der Attributsliste fürs Pixelformat (also mit abschließender 0 am Ende). Für unseren Fall sieht diese Liste so aus:

PixelBufferAttribs : array[0..4] of TGLUInt =
(WGL_TEXTURE_FORMAT_ARB, WGL_TEXTURE_RGBA_ARB,
WGL_TEXTURE_TARGET_ARB, WGL_TEXTURE_2D_ARB, 0);

Und hier noch ein paar der möglichen Attribute für unseren Pixelpuffer und ihre Bedeutung:

WGL_TEXTURE_FORMAT_ARB

Gibt das Format der Textur an die erstellt wird wenn der Pixelpuffer an diese gebunden wird. Möglich ist hier entweder WGL_TEXTURE_RGBA_ARB oder WGL_TEXTURE_RGB_ARB, je nachdem ob man nen Alphakanal benötigt oder nicht. Gültig ist hier auch noch der Parameter WGL_NO_TEXTURE_ARB, der allerdings im Normallfall kaum Sinn macht.

WGL_TEXTURE_TARGET_ARB

Gibt das Texturenziel für die Textur an die der Pixelpuffer gebunden wird an. Möglich sind hier WGL_NO_TEXTURE_ARB (wie oben recht sinnlos in unserem Falle), WGL_TEXTURE_1D_ARB für 1D-Texturen und respektive (und auch der Normalfall) WGL_TEXTURE_2D_ARB für 2D-Texturen. Außerdem ist noch WGL_TEXTURE_CUBE_MAP_ARB verfügbar, wenn man mittels Pixelpuffern dynamische Cubemaps erstellen will.

WGL_MIPMAP_TEXTURE_ARB

Wenn diese Eigenschaften auf GL_TRUE gesetzt wird (Grundeinstellung GL_FALSE), dann wird Platz für Mipmaps (die man dann z. B. über die automatische Mipmap-generation erstellen kann) reserviert.

WGL_PBUFFER_LARGEST_ARB

Wenn diese Eigenschaft auf 'GL_TRUE gesetzt wird, dann versucht die GL-Implementation bei Platzmangel (also wenn der Pixelpuffer in der angeforderten Größe nicht mehr in den Speicher passt, oder die Hardware solche Größen nicht unterstützt) einen Pixelpuffer zu erstellen der zwar die angeforderten Eigenschaften (und Pixelformat) besitzt, allerdings jedoch eine Nummer kleiner (bzw. in einer Größe die von der HW machbar ist und die im Speicher Platz findet).

Haben wir uns also auf die Eigenschaften des PPuffers festgelegt, so können wir mit wglCreatePBufferARB einen passenden Puffer von der GL anfordern :

Handle := wglCreatePBufferARB(TempDC, PFormat[0], Width, Height, @PixelBufferAttribs);

Im Erfolgsfalle bekommen wir dann ein Handle auf den Pixelpuffer zurückgeliefert, das größer 0 ist. Sollte das Handle = 0 sein, so konnte uns die GL-Implementation keinen Pixelpuffer mit dem angeforderten Format bereitstellen und wir müssen unseren Versuch hier abbrechen.

Den größten Teil der Arbeit haben wir nun hinter uns. Was wir jetzt nur noch brauchen wären sowohl ein eigener Gerätekontext für den Pixelpuffeer als auch einen eigenen Renderkontext. Das geht aber zum Glück sehr einfach:

DC := wglGetPBufferDCARB(Handle);
...
RC := wglCreateContext(DC);

Einfacher geht's also kaum. Aber auch hier sollte man natürlich prüfen obwohl sowohl der Gerätekontext als auch der Renderkontext > 0 sind, denn sonst konnten sie nicht erstellt werden.

Schlussendlich müssen wir dann noch eine Textur erstellen die quasi die Schnittstelle zu unserem Pixelpuffer darstellt. Das wird allerdings das erste und letzte Mal (in unserem Falle) sein das wir mit der Texturen-ID unseres Pixelpuffers in Berührung kommen, denn so wie wir den Puffer erstellt haben (sprich: dessen Eigenschaften) müssen wir diese Texturen-ID später weder binden noch den Pixelpufferinhalt in diese hinein kopieren:

glGenTextures(1, @TextureID);
glBindTexture(GL_TEXTURE_2D, TextureID);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

Zu obigem Quellcode gibt's dann nicht viel zu sagen, das sind ja Grundlagen die jeder an diesem Tutorial interessierte bereits seit längerer Zeit sein eigen nennen sollte.

Und gleichzeitig stellen diese Zeilen auch den Abschluss der Erstellung des Pixelpuffers da. Dieser ist nun einsatzbereit und wartet geradezu auf seine Nutzung.

Den Pixelpuffer zerstören

Wie immer sollte man nicht mehr verwendete Ressourcen spätestens beim Schließen des Programms freigeben, so auch unseren Pixelpuffer. Das sieht dann wie gewohnt ähnlich der Freigabe eines normalen Renderkontextes aus:

destructor TpixelBuffer.Destroy;
begin
  wglDeleteContext(RC);
  wglReleasePbufferDCARB(Handle, DC);
  wglDestroyPbufferARB(Handle);
  Log.Free;
  inherited;
end;

Zuerst löschen wir den Renderkontext der zum Pixelpuffer gehört, dann geben wir dessen Gerätekontext frei um schlussendlich dann den Pixelpuffer selbst aus dem Grafikkartenspeicher zu entfernen.

Verlorener Pixelpuffer

Es kann unter Umständen vorkommen, dass unser Pixelpuffer in bestimmten Situationen ungültig wird. Das ist meist dann der Fall, wenn während der Laufzeit einer GL-Anwendung die Auflösung gewechselt wird. Darauf sollte man natürlich reagieren, und anhand einer Abfrage lässt sich über wglQueryPbufferARB feststellen, ob unser Pixelpuffer ungültig geworden ist:

function TPixelBuffer.IsLost : Boolean;
var
  Flag : TGLUInt;
begin
  Result := False;
  wglQueryPbufferARB(Handle, WGL_PBUFFER_LOST_ARB, @Flag);
  if Flag <> 0 then
    Result := True;
end;

In den Pixelpuffer rendern

Kommen wir nun also zum interessantes Teil. Denn den Pixelpuffer nutzt man ja um etwas hineinzurendern, was dann in Form einer Textur für spätere Zwecke genutzt werden soll. Wie schon vorher gesagt, besitzt der Pixelpuffer einen eigenen (und damit total unabhängigen) Renderkontext, der dann logischerweise auch seine eigene GL-Statemachine mit sich bringt und einen eigenen Namespace besitzt. Im Normalfall ist es aber so das man im Kontext des Pixelpuffers meist auch die gleichen Texturen, Displayliste (oder VBOs), Shader, usw. wie im Hauptrenderkontext benötigt. Um dies zu erreichen müssen wir der GL mitteilen das sich zwei Renderkontexte einen Namespace teilen wollen. Unglücklicherweise ist der Name der Funktion, wglShareLists, etwas irreführend, und war auch eigentlich eher gedacht (wie der Name vermuten lässt) Displaylisten über verschiedene Kontexte nutzen zu können. Man hat sich dann allerdings dazu entschieden die Funktionalität von wglShareLists ganz einfach zu erweitern, und so kann man mittels dieser Funktion nun auch Texturen, Shader, uvm. zwischen zwei Renderkontexten teilen:

wglShareLists(RC, PixelBuffer.RC);

Sorgt also dafür das im Hauptkontext erstellte „Ressourcen“ (besser passt hier GL-Namen, also Texturen-IDs, Listen-IDs, usw.) auch im Renderkontext des Pixelpuffers nutzbar sind und umgekehrt.

Kommen wir nun zum eigentlichen Renderablauf bei der Nutzung eines Pixelpuffers, der zumindest dann gilt wenn man nur eine Textur erstellen möchte, die man dann später irgendwo und irgendwie anzeigt (Spezialfälle wie z. B. Rendern der Szenentiefe für Shadowmapping werden später kurz angesprochen):

Schritt 1: Den Pixelpuffer „aktivieren“

Dies geschieht genau wie mit einem normalen Renderkontext. Man ruft die Funktion wglMakeCurrent auf und übergibt dieser sowohl den Geräte- als auch den Renderkontext des Puffers. In unserer Klasse kapseln wir dies der Einfachheit halber in der Funktion TPixelBuffer.Enable.

Schritt 2: Die Szene rendern

Wie gewohnt rendern wir die Szene, die wir später auf der Textur sehen wollen. Da jetzt unser Pixelpuffer aktiv ist, wird natürlich auch in dessen Puffer gerendert, während unser Hauptkontext unangetastet bleibt. Ein SwapBuffers sparen wir uns natürlich, da wir ja einen nicht sichtbaren Pixelpuffer haben und doppelte Pufferung dort keinen Sinn macht und daher von uns auch nicht genutzt wird.

Schritt 3: Den Pixelpuffer deaktiveren

An sich ist die Bezeichnung etwas irreführend, aber so kapselt man es am besten. Hier wird nämlich nichts weiter getan als wglMakeCurrent mit dem Geräte- und Renderkontext der Hauptanwendung aufzurufen, was im Endeffekt bedeutet das unser Pixelpuffer inaktiv ist und wieder in den Hauptkontext gerendert wird.

Schritt 4: Den Pixelpuffer binden

Wenn wir den Pixelpuffer nun als Textur auf einer Primitiven darstellen wollen, so binden wir nicht wie gewohnt dessen Texturen-ID, sondern nutzen wglBindTexImageARB um den Pixelpuffer direkt als Textur verwenden zu können. Gekapselt wird das von unserer Klasse aussagekräftig in TPixelBuffer.Bind, wobei der zweite Parameter von wglBindTexImageARB mit WGL_FRONT_LEFT_ARB angegeben wird. Dies ist der Puffer in den wir normalerweise rendern, und alle anderen dort gültigen Variablen kommen nur dann in Frage wenn wir z. B. ein stereoskopisches Bild (3D-Brille/Display) rendern wollen.

Schritt 5: Die mit dem Pixelpuffer texturierte Primitive rendern

Nun kommen wir endlich zu unserem Ziel. Nachdem der Pixelpuffer als Textur gebunden wurde, rendern wir unsere Primitive. Ob GL_QUAD, GL_TRIANGLE oder sonst was ist dann jedem überlassen, solange natürlich die Texturkoordinaten stimmen. Wir sollten jetzt also das was wir in Schritt 2 in den Pixelpuffer gerendert haben auf unserer Primitiven zu sehen bekommen (nicht vergessen : Im Hauptkontext muss natürlich GL_TEXTURE_2D aktiviert worden sein, sonst gibt's nur ne weiße Fläche).

Schritt 6: Den Pixelpuffer „entbinden“

Zum Schluss müssen wir dann noch unseren Pixelpuffer via wglReleaseTexImageARB „entbinden“ (gekapselt in TPixelBuffer.Release). Dies ist sehr wichtig, denn wird dies vergessen, so schlagen alle weiteren Versuche, in den Pixelpuffer zu rendern, fehl.

Anwendungsbeispiel: Tiefenpuffer in eine Textur kopieren

Bevor ich das Tutorial zu Ende bringe, möchte ich noch ein praktisches und oft verwendetes Beispiel zeigen für das Pixelpuffer verwendet werden: Nämlich den Inhalt des Tiefenpuffers einer Szene in eine Textur zu kopieren. Genutzt wird das im Normalfall für projektives (also dynamisches) Shadowmapping.

Aber warum sollte man dafür einen Pixelpuffer nutzen, werden sich sicherlich einige Fragen. Dafür gibt es zwei Gründe: Es gibt zwar eine eigene Extension, mit der man ganz einfach den Inhalt des Tiefenpuffers in eine RGB-Textur bringen kann (GL_NV_copy_depth_to_color), aber wie so oft ist das herstellerabhängig. Außerdem braucht man oft recht hochaufgelöste Shadowmaps, und da kann man nur mit einem Pixelpuffer frei agieren. Wie ich in der Einleitung nämlich bereits erwähnte, muss man sich bei normalem Render-To-Texture über glCopyTexImage an die größe des GL-Viewports und auch an die Dimension 2^n*2^n halten (es sei denn man benutzt spezielle Extensions), was natürlich sehr stark einschränkt. Wenn die Anwendung also z. B. mit einer Viewportgröße von 1024x768 Pixeln läuft, dann bleibt einem (es sei denn man trickst) nichts anders übrig als auf die nächstkleinere passende Dimension, also in diesem Falle 512x512 Pixel auszuweichen. Klingt erstmal nach ausreichender Auflösung, aber in 640x480 müsste man dann schon auf 256x256 Pixel ausweichen, und oft sind selbst 1024x1024 Pixel nicht genug für eine Shadowmap. Aber genau hier springen schnell die Vorteile eines Pixelpuffers ins Auge. Die Auflösung ist unabhängig von der Viewportgröße (also egal ob man in 640x480 oder 1024x768 rendert) und durch den eigenen Kontext kann man hier sogar ohne großen Verwaltungsaufwand fürs Shadowmapping optimierte Objekte rendern und alles was man für die Tiefeninfo nicht braucht (Licht, Texturen) deaktivieren, ohne das man dies in jedem Frame tun muss.

Implementierung

Da wir die Tiefeninformationen des Pixelpuffers leider nicht direkt als RGB-Textur verwenden können, müssen wir das mit einem Umweg über eine extra Textur machen :

glGenTextures(1, @Tex);
glBindTexture(GL_TEXTURE_2D, Tex);
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 0, 0, PixelBuffer.Width, PixelBuffer.Height, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

Diese Textur werden wir dann gleich nutzen um die Tiefeninformationen aus dem Pixelpuffer zu übernehmen. Natürlich belegen wir die Textur mit einem glCopyTexImage2D vor, damit wir später das schneller arbeitende glCopySubTexImage verwenden können. GL_DEPTH_COMPONENT dürfte auch recht logisch erscheinen, denn dies sagt der GL das wir den Inhalt des Tiefenpuffers in die Textur kopieren wollen.

Nun aktivieren wir unseren Pixelpuffer (TPixelBuffer.Enable) und rendern die Szene aus der Sicht der Lichtquelle (bei der Erstellung des Pixelpuffers haben wir natürlich alle unnötigen Dinge wie Texturierung und Beleuchtung deaktiviert und ggf. optimierte Modelle für die Schattendarstellung geladen). Dann kopieren wir den Inhalt des Tiefenpuffers unseres Pixelpuffers in unsere oben erstellte Textur:

glBindTexture(GL_TEXTURE_2D, Tex);
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, PixelBuffer.Width, PixelBuffer.Height);
PixelBuffer.Disable;

Jetzt haben wir die Tiefeninformationen aus Sicht der Lichtquelle in unserem Texturenobjekt Tex, das später dann nur noch mit den passenden (zum Thema Shadowmapping gibt's ja im Netz genug Infos, deshalb spare ich mir hier eine Erklärung) Vergleichsfunktionen auf die Szene projizieren muss. Wie man sieht kann man also mit dem Pixelpuffer nun unabhängig von der Auflösung Shadowmaps in (fast) beliebiger Größe erstellen (natürlich nur so groß wie die Hardware kann).

Schlusswort

Das wars also zum Thema Pixelpuffer. Wie dem ein oder anderem evtl. aufgefallen ist, wurde Render-To-Texture via PPuffer leider nicht sonderlich schön in die sonst so logische und gut durchdachte OpenGL-API eingebunden, allerdings wurde das erkannt und es ist bereits eine neue Extension auf dem Weg die den Pixelpuffer und seine recht klobige API ablösen soll. Allerdings wird das noch dauern und es ist auch fraglich ob aktuelle Hardware das dann unterstützen wird. Technisch sollte das der Fall sein, aber das hängt wie immer vom Hersteller ab. Solange muss man sich also mit dem Pixelpuffer begnügen, und ich hoffe einigen die damit Probleme hatten durch dieses Tutorial etwas geholfen zu haben. Wie immer ist Feedback (→ Forum) ausdrücklich erwünscht, denn das hatte besonders bei meinen letzten Tutorials stark zu wünschen gelassen.

Euer

Sascha Willems (webmaster_at_delphigl.de)

Dateien


Vorhergehendes Tutorial:
Tutorial_NVOcclusionQuery
Nächstes Tutorial:
Tutorial_Framebufferobject

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