OpenGL-Frame-Buffer- und -Render-Buffer-Objekte

Frame-Buffer- und Render-Buffer-Objekte sind für uns OpenGL-Programmierer die Eintrittskarte in die Welt der modernen Grafik-Programmierung, ermöglichen sie doch Render-To-Texture-Techniken wie Shadow Mapping, Environment Mapping, Post Processing, eine realistische Wasserdarstellung, Deferred Lighting (Shading), usw.
Schön und gut, mögen einige von Ihnen jetzt denken, aber wieso benötigt man hierfür zwei OpenGL-Buffer-Objekte? Würde ein einzelnes Objekt nicht reichen?
In diesem Zusammenhang muss man sich jedoch in Erinnerung rufen, dass bei der Szenendarstellung ohne Frame Buffer und Render Buffer ebenfalls zwei Buffer-Objekte zum Einsatz kommen – der Depth Buffer (z-Buffer), in dem die Tiefenwerte der Szene gespeichert werden und der „Color Buffer“ zum Speichern der Farbwerte (des Szenenbildes). Sichtbar ist für uns lediglich das Szenenbild. Der z-Buffer verrichtet seine Arbeit im Hintergrund und sorgt dafür, dass verdeckte Oberflächen (Pixel) nicht gerendert werden.
Soll nun das Szenenbild in eine Textur gerendert werden, so übernimmt der Render Buffer die Aufgabe des z-Buffers und der Frame Buffer die des Color-Buffers. Im Unterschied zum Render Buffer kann man an ein Frame-Buffer-Objekt ein oder gar mehrere Texturen binden. Diese Texturen dienen als Renderziele (Render Targets) – mit anderen Worten, sie speichern das Szenenbild.

Hinweis:
Viele moderne Render-To-Texture-Techniken sind nur deshalb echtzeitfähig, weil es möglich ist, mehrere Texturen (Render Targets) an einen Frame Buffer zu binden. Mithilfe eines geeigneten Fragment-Shaders ist es möglich, gezielt Farbwerte in die einzelnen Render Targets zu schreiben (bsp. Target 1: Szenenbild, Target 2: Tiefenabbild der Szene, usw.) (siehe u. a. hierzu das OpenGL Tutorial Post Processing, Multiple Render Targets).
Die Anzahl der Texturen, die maximal an ein Frame-Buffer-Objekt gebunden werden können, lässt sich wie folgt bestimmen:

GLuint maxbuffers;
glGetIntergeri(GL_MAX_COLOR_ATTACHMENTS, &maxbuffers);


In unserem ersten Beispiel werden wir 2 Texturen als Render Targets an einen Frame Buffer binden. Textur 1 dient zum Speichern des Szenenbildes und Textur 2 zum Speichern des zugehörigen Tiefenabbildes.

// ID des Render-Buffer-Objekts:
GLuint PrimaryScreenRenderBuffer;

// ID des Frame-Buffer-Objekts:
GLuint PrimaryScreenFrameBuffer;

. . .


// 2 leere Textur-Objekte (Render-Targets) erzeugen:

// Textur 1 dient zum Speichern des Szenenbildes
PrimaryScreenTexture = new CTexture;
PrimaryScreenTexture->Create_Empty_RGBA_Texture(g_screenwidth,
                                                g_screenheight);

// Textur 2 dient zum Speichern des Tiefenabbildes:
PrimaryCameraDepthTexture = new CTexture;
PrimaryCameraDepthTexture->Create_Empty_HDR_Texture(g_screenwidth,
                                                    g_screenheight);

// Render Buffer initialisieren:
glGenRenderbuffers(1, &PrimaryScreenRenderBuffer);

glBindRenderbuffer(GL_RENDERBUFFER, PrimaryScreenRenderBuffer);

// Der Render Buffer übernimmt die Funktion des z-Buffers:
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24,
                      g_screenwidth, g_screenheight);



// Frame Buffer initialisieren:

glGenFramebuffers(1, &PrimaryScreenFrameBuffer);

glBindFramebuffer(GL_FRAMEBUFFER, PrimaryScreenFrameBuffer);

// Textur 1 als Render Target binden:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                       PrimaryScreenTexture->TextureID, 0);

// Textur 2 als Render Target binden:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D,
                       PrimaryCameraDepthTexture->TextureID, 0);

// den Render Buffer in seiner Funktion als z Buffer an den Frame Buffer
// anbinden:

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                          GL_RENDERBUFFER, PrimaryScreenRenderBuffer);

// Überprüfen, ob der Frame Buffer korrekt erzeugt wurde:
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    Add_To_Log("Framebuffer unsupported");
else
// Test abgeschlossen:
    Add_To_Log("glCheckFramebufferStatus(...); OK");

glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);


Werden Frame Buffer und Render Buffer nicht mehr benötigt, so muss der von ihnen belegte Speicherplatz natürlich wieder freigegeben werden:

glDeleteRenderbuffers(1, &PrimaryScreenRenderBuffer);
glDeleteFramebuffers(1, &PrimaryScreenFrameBuffer);


Im zweiten Beispiel werden wir eine Cube Map an einen Frame Buffer binden. Cube Maps bestehen gewissermaßen aus 6 Einzeltexturen und sind dadurch in der Lage, einen „Rundumblick“ der 3D-Szene zu speichern. Jede dieser Einzeltexturen speichert hierbei einen 90°-Ausschnitt der 3D-Szene aus einem anderen Blickwinkel (positive und negative x-, y- und z-Richtungen). Mittels der folgenden OpenGL-Konstanten kann man einer Einzeltextur den jeweiligen Blickwinkel zuweisen:

GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,

Hinweis:
Beim Erzeugen der Cube Maps (siehe Artikel OpenGL-Textur-Objekte) haben wir uns folgende Identitäten zunutze gemacht:

GL_TEXTURE_CUBE_MAP_POSITIVE_X+1 = GL_TEXTURE_CUBE_MAP_NEGATIVE_X
GL_TEXTURE_CUBE_MAP_POSITIVE_X+2 = GL_TEXTURE_CUBE_MAP_POSITIVE_Y
GL_TEXTURE_CUBE_MAP_POSITIVE_X+3 = GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
usw.

Für jede der 6 Cube-Map-Einzeltexturen müssen wir einen separaten Frame und Render Buffer erzeugen:


// Render-Buffer- und Frame-Buffer-ObjektID-Variablen:

GLuint RenderBuffer_PosX;
GLuint FrameBuffer_PosX;

GLuint RenderBuffer_NegX;
GLuint FrameBuffer_NegX;

GLuint RenderBuffer_PosY;
GLuint FrameBuffer_PosY;

GLuint RenderBuffer_NegY;
GLuint FrameBuffer_NegY;

GLuint RenderBuffer_PosZ;
GLuint FrameBuffer_PosZ;

GLuint RenderBuffer_NegZ;
GLuint FrameBuffer_NegZ;

. . .

// leeres Cube-Map-Textur-Objekt erzeugen:
EnvironmentCubeTexture = new CTexture;
EnvironmentCubeTexture->Create_Empty_CubeTexture(CubeTextureResolution);

// Eine Cube Map setzt sich aus 6 Einzeltexturen zusammen. Für jede dieser
// Texturen wird ein separater Frame und Render Buffer erzeugt:

// Blickrichtung positive x-Achse:

glGenRenderbuffers(1, &RenderBuffer_PosX);

glBindRenderbuffer(GL_RENDERBUFFER, RenderBuffer_PosX);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24,
                      CubeTextureResolution, CubeTextureResolution);

glGenFramebuffers(1, &FrameBuffer_PosX);
glBindFramebuffer(GL_FRAMEBUFFER, FrameBuffer_PosX);

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                       GL_TEXTURE_CUBE_MAP_POSITIVE_X,
                       EnvironmentCubeTexture->TextureID, 0);

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                          GL_RENDERBUFFER, RenderBuffer_PosX);

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    Add_To_Log("Framebuffer unsupported");

glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// ebenso für die weiteren Blickrichtungen . . .


Bevor die aktuelle Szene in die aus Beispiel 1 bekannte PrimaryScreenTexture gerendert werden kann, sind einige Vorbereitungen erforderlich. Zunächst müssen die im z-Buffer (Render Buffer) gespeicherten Tiefenwerte gelöscht werden (zu diesem Zweck werden alle Werte mit dem maximal möglichen Tiefenwert 1.0 überschrieben). Des Weiteren überschreiben wir die im Render Target gespeicherten Farbwerte mit der gewählten Hintergrundfarbe. In unseren Programmbeispielen ist hierfür die Prepare_PrimarySceneRendering()-Methode der CPostProcessingEffects-Klasse verantwortlich:

void CPostProcessingEffects::Prepare_PrimarySceneRendering(
                             D3DXVECTOR4* pBackgroundColor)
{
if(g_NumActiveRenderTargets > 0)
    Add_To_Log("too many active render targets",
               &g_NumActiveRenderTargets);

glBindFramebuffer(GL_FRAMEBUFFER, PrimaryScreenFrameBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, PrimaryScreenRenderBuffer);

// Viewport setzen, Höhe und Breite sind abhängig von der Größe
// des jeweiligen Renter Targets (Textur):
glViewport(0, 0, g_screenwidth, g_screenheight);

// Render Target (PrimaryScreenTexture) auswählen:
glDrawBuffer(GL_COLOR_ATTACHMENT0);

glClearColor(pBackgroundColor->x, pBackgroundColor->y,
             pBackgroundColor->z, pBackgroundColor->w);

// PrimaryScreenTexture mit der gewählten Hintergrundfarbe füllen.
// Alle z-Buffer-(Render-Buffer-)Einträge mit dem maximal möglichen
// Tiefenwert von 1.0 überschreiben:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// GL_DEPTH_BUFFER_BIT: In der Funktion
// Set_Initial_OpenGL_RenderParameter() haben
// wir hierfür einen Leerwert 1.0f festgelegt:
// glClearDepth(1.0f);

g_NumActiveRenderTargets++;

Stop_PrimarySceneRendering();
}


In ähnlicher Weise wird in der Prepare_PrimaryCameraDepthRendering()-Methode der CPostProcessingEffects-Klasse die PrimaryCameraDepthTexture für den neuen Render-Durchgang vorbereitet:

void CPostProcessingEffects::Prepare_PrimaryCameraDepthRendering(void)
{
if(g_NumActiveRenderTargets > 0)
    Add_To_Log("too many active render targets",
               &g_NumActiveRenderTargets);

glBindFramebuffer(GL_FRAMEBUFFER, PrimaryScreenFrameBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, PrimaryScreenRenderBuffer);

glViewport(0, 0, g_screenwidth, g_screenheight);

// Render Target (PrimaryCameraDepthTexture) auswählen:
glDrawBuffer(GL_COLOR_ATTACHMENT1);

glClearColor(0.0f, 0.0f, 0.0f, -1.0f);
glClear(GL_COLOR_BUFFER_BIT);

g_NumActiveRenderTargets++;
Stop_PrimarySceneRendering();
}


Ein Render-To-Texture-Vorgang kann jederzeit unterbrochen werden. Zu diesem Zweck müssen Frame und Render Buffer vom Render-Prozess abgekoppelt werden. Als Beispiel hierfür betrachten wir die CPostProcessingEffects-Methode Stop_PrimarySceneRendering():

void CPostProcessingEffects::Stop_PrimarySceneRendering(void)
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);

glViewport(0, 0, g_screenwidth, g_screenheight);

g_NumActiveRenderTargets--;

if(g_NumActiveRenderTargets < 0)
    Add_To_Log("< 0 active render targets",
               &g_NumActiveRenderTargets);
}


Ein unterbrochener Render-To-Texture-Vorgang kann natürlich zu jeder Zeit wieder fortgeführt werden. Zu diesem Zweck müssen Frame Buffer und Render Buffer wieder an den Render-Prozess angebunden werden. Im Anschluss daran müssen die zu verwendenden Render Targets auswählt werden. Die CPostProcessingEffects-Methode Continue_PrimarySceneRendering() bietet in diesem Zusammenhang zwei Optionen an. Wird das sceneColorOnly-Flag auf true gesetzt, wird lediglich die PrimaryScreenTexture als Renderziel berücksichtigt; andernfalls fungieren sowohl die PrimaryScreenTexture als auch die PrimaryCameraDepthTexture gleichsam als Renderziele:

void CPostProcessingEffects::Continue_PrimarySceneRendering(
                             bool sceneColorOnly)
{
if(g_NumActiveRenderTargets > 0)
    Add_To_Log("too many active render targets:",
               &g_NumActiveRenderTargets);

glBindFramebuffer(GL_FRAMEBUFFER, PrimaryScreenFrameBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, PrimaryScreenRenderBuffer);

if(sceneColorOnly == true)
{
     // Render Target (PrimaryScreenTexture) auswählen:
    glDrawBuffer(GL_COLOR_ATTACHMENT0);
    glViewport(0, 0, g_screenwidth, g_screenheight);
}
else
{
    // mehrere Render Targets (PrimaryScreenTexture,
    // PrimaryCameraDepthTexture) auswählen:
    GLenum buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
    glDrawBuffers(2, buffers);
    glViewport(0, 0, g_screenwidth, g_screenheight);
}

g_NumActiveRenderTargets++;
}

Soll nun ein Fragment Shader in mehrere Render Targets schreiben können, benötigen wir zunächst eine entsprechende Anzahl von Output-Variablen (gs_FragColor[2]). In unserem Beispiel schreibt gs_FragColor[0] in die PrimaryScreenTexture, da wir in buffers[] GL_COLOR_ATTACHMENT0 als erstes Renderziel festgelegt haben. Dementsprechend schreibt gs_FragColor[1] in die PrimaryCameraDepthTexture, da wir in buffers[] GL_COLOR_ATTACHMENT1 als zweites Renderziel festgelegt haben.

// Output Variablen:
out vec4 gs_FragColor[2];

void main()
{
    // GL_COLOR_ATTACHMENT0:
    gs_FragColor[0] = vec4(0.0, 1.0, 0.0, 1.0);

    // GL_COLOR_ATTACHMENT1:
    gs_FragColor[1] = vec4(0.0, 0.0, 1.0, 1.0);
}