Tutorial Framebufferobject

Aus DGL Wiki
Wechseln zu: Navigation, Suche

GL_EXT_FRAMEBUFFER_OBJECT

Einleitung

Willkommen zu meinem ersten Tutorial. Das Thema des Tutorials sind Framebufferobjekte. Diese stellen derzeit die wohl einfachste Möglichkeit für Offscreen Rendering dar. Im Gegensatz zu Pixelbuffern sind sie plattformunabhängig und bieten automatisches Mipmapping. Jedoch benötigen sie mindestens OpenGL 1.1. Außerdem unterstützen Framebufferobjekte kein AntiAliasing. In der Vergangenheit hatten auch ATI Treiber einige Probleme mit ihnen, dies dürfte aber der Vergangenheit angehören.

Das FBO erstellen

Zuerst einmal muss das Framebufferobjekt natürlich generiert werden. Dies läuft ähnlich wie das Erstellen einer Textur ab:

var
  fbo: GluInt;
 
  glGenFramebuffersEXT(1, @fbo);

Wie bei allem anderen auch müssen wir das Framebufferobjekt jetzt binden, damit alles, was folgt sich auf dieses Objekt bezieht:

  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);

Der erste Parameter von glBindFramebufferEXT ist das Ziel an das gebunden wird. Derzeit existiert aber nur GL_FRAMEBUFFER_EXT.

Einen Depthbuffer hinzufügen

Ein einfaches Framebufferobjekt ist noch ziemlich nutzlos. Deswegen fügen wir zuerst einmal einen der neu eingeführten Renderbuffer als Depthbuffer hinzu. Renderbuffer sind im Prinzip Objekte, die benutzt werden wenn ein Buffer kein zugehöriges Texturformat besitzt. Dazu zählen u.a. der Stencilbuffer, der Accumulationbuffer und der Depthbuffer. Das Binden an den Accumulationbuffer wurde allerdings auf eine spätere Extension verschoben. Wie auch beim Framebufferobjekt müssen wir einen Renderbuffer zuerst generieren und dann binden:

var
  depthbuffer: GluInt;
 
  glGenRenderbuffersEXT(1, @depthbuffer);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depthbuffer);

Derzeit hat der Renderbuffer allerdings noch keinen Speicher zugeteilt bekommen. Deswegen sagen wir OpenGL, wie wir den Renderbuffer benutzen wollen und welche Größe er haben soll:

  glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, 512, 512);

Mit glRenderbufferStorageEXT hat OpenGL den Speicher für den Renderbuffer, den wir als Depthbuffer nutzen, reserviert. Jetzt können wir unseren Depthbuffer zum FBO hinzufügen:

  glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT,
  GL_RENDERBUFFER_EXT, depthbuffer);

Die letzte Anweisung sollte ziemlich einfach zu verstehen sein. Der zweite Parameter sagt einfach, als was der Renderbuffer genutzt werden soll.

Eine Textur hinzufügen

Um Farbinformationen in unser Framebufferobjekt schreiben zu können, fügen wir eine Textur hinzu. Bevor wir sie hinzufügen können müssen wir sie natürlich noch erstellen:

var
  tex: GluInt;
 
  glGenTextures(1, @tex);
  glBindTexture(GL_TEXTURE_2D, tex);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8,  512, 512, 0, GL_RGBA8, GL_UNSIGNED_BYTE, nil);

Es dürfte eigentlich nichts völlig Unverständliches da stehen. Wir erstellen die Textur, binden sie, stellen die Filtermodi ein und erzeugen im Speicher den entsprechenden Platz. Anzumerken ist aber, dass die Textur die gleiche Größe wie der Renderbuffer haben muss. Alle Größen müssen einheitlich sein. Man kann also nicht einen Renderbuffer von 512*512 und eine Textur von 256*256 erstellen und sie zusammen in einem FBO nutzen. Hinzugefügt wird sie nach dem gleichen Schema wie der Depthbuffer:

  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, tex, 0);

Wie gesagt, nicht viel neues hier. Der zweite Parameter, GL_COLOR_ATTACHMENT0_EXT sagt OpenGL wo die Textur hinzugefügt werden soll (man kann mehrere hinzufügen, doch dazu später mehr). Der letzte Parameter beschreibt das Mipmaping-Level.

Die Fehlerabfrage

Derzeit wissen wir nicht, ob alles so geklappt hat, wie wir es wollten oder ob wir einen Fehler gemacht haben. Deswegen ist nun die Fehlerabfrage an der Reihe. Dazu gibt es die Funktion glCheckFramebufferStatusEXT. Die Fehlerabfrage lässt sich schön in so einer Prozedur verpacken:

procedure TForm1.CheckFBO;
var
  error: GlEnum;
begin
  error := glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
  case error of
    GL_FRAMEBUFFER_COMPLETE_EXT:
      Exit;
    GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
      raise Exception.Create('Incomplete attachment');
    GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
      raise Exception.Create('Missing attachment');
    GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
      raise Exception.Create('Incomplete dimensions');
    GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
      raise Exception.Create('Incomplete formats');
    GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
      raise Exception.Create('Incomplete draw buffer');
    GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
      raise Exception.Create('Incomplete read buffer');
    GL_FRAMEBUFFER_UNSUPPORTED_EXT:
      raise Exception.Create('Framebufferobjects unsupported');
  end;
end;

Die Codeübersicht

Zur Übersicht noch einmal der komplette Code zum erstellen eines Framebufferobjekts:

var
  fbo: 		GluInt;
  depthbuffer: 	GluInt;
  tex: 		GluInt;
 
procedure TForm1.InitFBO;
begin
  glGenFramebuffersEXT(1, @fbo);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
  glGenRenderbuffersEXT(1, @depthbuffer);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depthbuffer);
  glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, 512, 512);
  glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, depthbuffer); 
  glGenTextures(1, @tex);
  glBindTexture(GL_TEXTURE_2D, tex);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8,  512, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, nil);
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
  GL_TEXTURE_2D, tex, 0);
  CheckFBO;
end;

In die Textur rendern

Der eigentliche Rendervorgang ist ziemlich simpel und unterscheidet sich nicht sonderlich vom normalen Rendern. Das Framebufferobjekt teilt sich alle Einstellungen mit dem eigentlichen Renderingkontext. Deswegen muss eigentlich nur das FBO gebunden und der Viewport auf die entsprechende Breite und Höhe eingestellt werden:

  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
  glPushAttrib(GL_VIEWPORT_BIT);
  glViewPort(0, 0, 512, 512);
 
  //ganz normal rendern – das Ergebnis landet in der Textur
 
  glPopAttrib;
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

Wie gesagt, hier gibt’s nichts bemerkenswertes: Viewport speichern, umstellen, rendern, Viewport laden und mit der letzten Zeile das FBO abschalten. Die Textur die an das Framebufferobjekt gebunden wurde, enthält das Gezeichnete und wird ganz normal mit glBindTexture(GL_TEXTURE_2D, tex) benutzt.

Mipmaps

FBOs bieten die Möglichkeit zum automatischem Erstellen der Mipmapdaten. Dazu gibt es die Funktion glGenerateMipmapEXT. Man bindet die Textur zunächst und ruft dann die Funktion auf:

procedure TForm1.Render;
begin
  //...
  glBindTexture(GL_TEXTURE_2D, tex);
  glGenerateMipmapEXT(GL_TEXTURE_2D);
 
  //Textur danach ganz normal benutzen
 
end;

Wenn man hingegen einen der Mipmap Filter verwendet muss der Befehl glGenerateMipmapEXT bevor der Status des FBOs überprüft oder versucht wird in die Textur zu rendern aufgerufen werden. Ein Beispiel wäre folgender Code in der Initialisierung des Framebufferobjektes:

var
  fbo: 	GluInt;
  depthbuffer: 	GluInt;
  tex: 	GluInt;
 
  glGenTextures(1, @tex);
  glBindTexture(GL_TEXTURE_2D, tex);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 512, 512, 0, GL_RGBA, 	GL_UNSIGNED_BYTE, nil);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_MIPMAP_LINEAR);
  glGenerateMipmapEXT(GL_TEXTURE_2D);

Und wieder aufräumen

Beim Beenden unseres Programms soll das alles natürlich auch wieder gelöscht werden:

  glDeleteFramebuffersEXT(1, @fbo);
  glDeleteRenderbuffersEXT(1, @depthbuffer);
  glDeleteTextures(1, @tex);

Ich denke das Ganze ist selbsterklärend.

Mehrere Texturen in einem FBO

Wie vorhin erwähnt gibt es auch die Möglichkeit mehrere Texturen an ein Framebufferobjekt zu binden. Dazu schauen wir uns nochmal den Befehl zum hinzufügen einer Textur an:

  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, tex, 0);

Der zweite Parameter sagt OpenGL an welches COLOR_ATTACHMENT die Textur zugefügt wird. Die derzeitigen Spezifikationen erlauben bis zu 16 COLOR_ATTACHMENTS (GL_COLOR_ATTACHMENT0_EXT bis GL_COLOR_ATTACHMENT15_EXT). Allerdings ist die wirkliche Anzahl von möglichen COLOR_ATTACHMENTS durch die Grafikkarte und deren Treiber begrenzt. Die maximale Anzahl kann durch folgenden Code ermittelt werden:

var
  maxbuffers: GluInt;
 
  glGetIntegeri(GL_MAX_COLOR_ATTACHMENTS_EXT, @maxbuffers);

Eine zweite, wie oben beschrieben erstellte Textur, kann also mit folgendem Code zum Framebufferobjekt hinzugefügt werden:

  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT, GL_TEXTURE_2D, tex2, 0);

In die zweite Textur rendern

Mit dem Befehl glDrawBuffer(GL_COLOR_ATTACHMENTn_EXT) lässt sich einstellen, in welchen Buffer man rendern möchte (wobei n den Buffer repräsentiert, also 0, 1, usw.). Standartmäßig ist GL_COLOR_ATTACHMENT0_EXT aktiviert (logisch, sonst hätten wir vorhin nicht viel gesehen, oder ;-) ). Soll also nur in diesen gerendert werden, ist keine weitere Anweisung nötig. Ansonsten sieht der Code folgendermaßen aus:

  glBindFramebuffer(GL_FRAMEBUFFER_EXT,  fbo);
  glPushAttrib(GL_VIEWPORT_BIT or GL_COLOR_BUFFER_BIT);
  glViewPort(0, 0, 512, 512);
 
  //in die Textur von GL_COLOR_ATTACHMENT0_EXT rendern
 
  glDrawBuffer(GL_COLOR_ATTACHMENT1_EXT);
 
  //in die Textur von GL_COLOR_ATTACHMENT1_EXT rendern 
 
  glPopAttrib;
  glBindFrameBuffer(GL_FRAMEBUFFER_EXT, 0);

Diesmal werden Viewport und Colorbuffer gespeichert, da Veränderungen immer das Framebufferobjekt und den eigentlichen Renderingkontext betreffen. Veränderungen am RC sind aber nicht gewollt, deswegen wird beides gespeichert und später wieder geladen. Wie beim normalen Renderpass ist der Colorbuffer bereits mit den Daten die in die erste Textur geschrieben werden gefüllt. Deswegen leert man diesen normalerweise vor dem Rendern in die zweite Textur mithilfe von glClear:

  glBindFramebuffer(GL_FRAMEBUFFER_EXT,  fbo);
  glPushAttrib(GL_VIEWPORT_BIT or GL_COLOR_BUFFER_BIT);
  glViewPort(0, 0, 512, 512);
 
  //in die Textur von GL_COLOR_ATTACHMENT0_EXT rendern
 
  glDrawBuffer(GL_COLOR_ATTACHMENT1_EXT);
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
 
  //in die Textur von GL_COLOR_ATTACHMENT1_EXT rendern 
 
  glPopAttrib;
  glBindFrameBuffer(GL_FRAMEBUFFER_EXT, 0);

Es ist im übrigen schneller ein FBO für mehrere Texturen zu nutzen als mehrere FBOs zu erstellen und zwischen ihnen zu wechseln.

Multiple Render Targets

Mithilfe des Befehls glDrawBuffers kann in einem FBO gleichzeitig in mehrere Texturen gerendert werden. Die Namen der GL_COLOR_ATTACHMENTS werden in einem Array abgelegt und mit dem Befehl übergeben:

var
  buffers: Array[0..1] of GlEnum;
 
  buffers[0] := GL_COLOR_ATTACHMENT0_EXT;
  buffers[1] := GL_COLOR_ATTACHMENT1_EXT;
//...
  glBindFramebuffer(GL_FRAMEBUFFER_EXT,  fbo);
  glPushAttrib(GL_VIEWPORT_BIT or GL_COLOR_BUFFER_BIT);
  glViewPort(0, 0, 512, 512);
 
  glDrawBuffers(2, @buffers);
  //normal rendern
 
  glPopAttrib;
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

Die Hardware und der Treiber legen die Grenze für die Anzahl von Texturen in die gleichzeitig gerendert werden kann. Der Wert kann mit folgendem Code erhalten werden:

var
  maxbuffers: GluInt;
 
  glGetIntegeri(GL_MAX_DRAW_BUFFERS, @maxbuffers);

MRT und GLSL

Solange man mindestens für die OpenGL Version 2.0 (bzw. GLSL Version 1.10) schreibt, kann man mithilfe von GLSL gleichzeitig unterschiedliche Dinge in zwei Texturen rendern. Dazu benutzt man in GLSL anstelle von gl_FragColor den Array gl_FragData[]. Als Beispiel rendern wir ein rotes und ein grünes Viereck:

var
  buffers: Array[0..1] of GlEnum;
//...
  buffers[0] := GL_COLOR_ATTACHMENT0_EXT;
  buffers[1] := GL_COLOR_ATTACHMENT1_EXT;
//...
  glBindFramebuffer(GL_FRAMEBUFFER_EXT,  fbo);
  glPushAttrib(GL_VIEWPORT_BIT or GL_COLOR_BUFFER_BIT);
  glViewPort(0, 0, 512, 512);
 
  glDrawBuffers(2, @buffers);
  //Viereck zeichnen
 
  glPopAttrib;
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

Der aufmerksame Leser merkts - der Delphi Code hat sich eigentlich nicht verändert :-). Also auf zu den Shadern. Der Vertexshader enthält nur das Nötigste:

void main(void)
{
  gl_Position = ftransform();
}

Und der Fragmentshader sieht folgendermaßen aus:

void main(void)
{
  gl_FragData[0] = vec4( 1.0, 0.0, 0.0, 1.0);
  gl_FragData[1] = vec4( 0.0, 1.0, 0.0, 1.0);
}

Und siehe da – die Textur die zu GL_COLOR_ATTACHMENT0_EXT gehört bekommt die Daten für ein rotes Quad und die zweite Textur die Daten für ein grünes Quad. Eine Sache bleibt aber noch zu erwähnen: Es ist die Reihenfolge in der die Werte der COLOR_ATTACHMENTS übergeben werden, die bestimmt was gl_FragData[0] und was gl_FragData[1] ist. Wenn wir also geschrieben hätten

var
  buffers: Array[0..1] of GlEnum;
 
  buffers[0] := GL_COLOR_ATTACHMENT1_EXT;
  buffers[1] := GL_COLOR_ATTACHMENT0_EXT;

dann würde gl_FragData[0] GL_COLOR_ATTACHMENT1_EXT beeinflussen und gl_FragData[1] GL_COLOR_ATTACHMENT0_EXT.

Und fertig!

So! Geschafft. Schlussendlich bleibt zu sagen das Framebufferobjekte sehr nützliche Helfer sind, die sich relativ leicht handhaben lassen. Zum Abschluss möchte ich euch nochmal daran erinnern, dass dies mein erstes Tutorial war, haltet euch also ordentlich ran mit Feedback ;-)

Cya, Deathball

Quellen

Weiterführende Links


Vorhergehendes Tutorial:
Tutorial_Pixelbuffer
Nächstes Tutorial:
-

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