OpenGL-Uniform-Buffer-Objekte

Im heutigen Artikel werden wir uns mit der Verwendung der in OpenGL 3.1 eingeführten Uniform-Buffer-Objekte befassen. Nehmen wir einmal an, wir müssten eine komplexe 3D-Szene unter Verwendung verschiedener Shader rendern. Eine in diesem Zusammenhang recht triviale Angelegenheit ist die Einstellung der Beleuchtungsparameter für die korrekte Ausleuchtung der Szene. Vor Einführung des Uniform-Buffer-Objektes dienten uniform-Variablen zum Speichern dieser Parameter. Die Verwendung dieser Variablen ist bei Licht betrachtet jedoch recht ineffizient, da man für jedes verwendete Shader-Programm dieselben Daten aufs Neue an die GPU schicken muss.

Die in einem Uniform Buffer gespeicherten Daten können bei Bedarf mehreren Shader-Programmen zur Verfügung gestellt werden. Aktualisiert man zu gegebener Zeit die Beleuchtungsparameter, so stehen diese Änderungen besagten Shader-Programmen gleichsam zur Verfügung.
Des Weiteren können in einem Uniform Buffer sehr viel mehr Daten gespeichert werden als in uniform-Variablen, was sowohl einen großen Vorteil bei der Animation von 3D-Objekten als auch beim Geometry Instancing bedeutet.
Der dritte Vorteil von Uniform-Buffer-Objekten macht sich bemerkbar, wenn zwischen verschiedenen Buffern gewechselt werden muss (Beispiel: Beleuchtung über dem Wasser bzw. im Wasser). Das Binden eines neuen Uniform-Buffer-Objekts erfolgt sehr viel schneller als die Änderung der in den uniform-Variablen gespeicherten Daten.

Mithilfe der nachfolgenden OpenGL-Funktionen können Sie ermitteln, wie viele Buffer-Objekte auf Ihrer jeweiligen Grafikkarte in den einzelnen Shader-Stages verarbeitet werden können und wie groß diese Buffer-Objekte maximal sein dürfen:

GLint x;

glGetIntegerv(GL_MAX_VERTEX_UNIFORM_BLOCKS, &x);
Add_To_Log("GL_MAX_VERTEX_UNIFORM_BLOCKS", &x);

glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_BLOCKS, &x);
Add_To_Log("GL_MAX_FRAGMENT_UNIFORM_BLOCKS", &x);

glGetIntegerv(GL_MAX_GEOMETRY_UNIFORM_BLOCKS, &x);
Add_To_Log("GL_MAX_GEOMETRY_UNIFORM_BLOCKS", &x);

glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &x);
Add_To_Log("GL_MAX_UNIFORM_BLOCK_SIZE", &x);


Der Zugriff auf die in einem Uniform Buffer gespeicherten Daten erfolgt innerhalb eines Shaders über einen Uniform Block, der seinerseits über einen sogenannten Binding Point mit dem Uniform Buffer verbunden ist. Stellen Sie sich einen Binding Point wie einen Knotenpunkt vor – müssen mehrere Shader-Programme auf die besagten Uniform-Buffer-Daten zugreifen, so lassen sich dementsprechend auch mehrere Uniform-Blöcke (und damit auch mehrere Shader) über einen Binding Point mit dem Uniform Buffer verbinden.
Schauen wir uns einmal an, wie man innerhalb eines Fragment Shaders auf die in einem Uniform Buffer gespeicherten Beleuchtungsparameter zugreifen kann:

#version 330

precision highp float;

in  vec4 gs_TexCoord[8];
out vec4 gs_FragColor;

uniform sampler2D AsteroidSurfaceTexture;
uniform sampler2D AsteroidNormalTexture;

// nachfolgender Uniform Block ermöglicht den Zugriff auf den Uniform
// Buffer, der die Lichteigenschaften speichert:
layout(shared) uniform lightingParam
{
    vec4 NegDirAndIntense;
    vec4 Color;
};
// Hinweis:
//mögliche memory layouts: shared (default), std140, packed

void main()
{

    mat3 matTexturespaceToWorldspace;

    // Perpendicular2:
    matTexturespaceToWorldspace[0] = gs_TexCoord[2].xyz;

    // Perpendicular1:
    matTexturespaceToWorldspace[1] = gs_TexCoord[3].xyz;

    // Normal:
    matTexturespaceToWorldspace[2] = gs_TexCoord[1].xyz;

    // lighting parameter aus uniform buffer auslesen:
    vec4 LightColor                = Color;
    vec3 negLightDir               = NegDirAndIntense.xyz;
    float BrightnessDistanceFactor = NegDirAndIntense.w;

    vec3 NormalPerTexel = matTexturespaceToWorldspace*
         (2.0*texture(AsteroidNormalTexture, gs_TexCoord[0].st).rgb-1.0);

    gs_FragColor = (0.4+BrightnessDistanceFactor*
                    max(dot(NormalPerTexel, negLightDir), 0.0))*
                    LightColor*
                    texture(AsteroidSurfaceTexture, gs_TexCoord[0].st);
}


Für die praktische Arbeit mit Uniform-Buffer-Objekten verwenden wir in unseren OpenGL-Tutorials die CUniformBufferObject-Klasse:

class CUniformBufferObject
{
public:

    GLuint  UniformBufferID;
    GLuint  UniformBlockIndex;
    GLsizei UniformBlockSize;

    long UniformBufferIndex;

    long BindingPoint;

    CUniformBufferObject();
    ~CUniformBufferObject();

    // Ermitteln des Uniform-Block-Index. Dieser Index ist notwendig,
    // damit der Uniform Buffer unter Berücksichtigung des Speicherbedarfs
    // eines in einem Shader definierten Uniform-Blocks initialisiert
    // werden kann.
    // Des Weiteren wird der Index benötigt, um den Buffer an ein
    // Shader-Programm zu binden!

    void GetUniformBlockIndex(CGLSLShader* pShader, char* BlockName);

    // Initialisierung eines Uniform-Buffers mit der Speichergröße eines
    // in einem Shader definierten Uniform-Blocks:
    void Init_UniformBuffer(CGLSLShader* pShader);

    // Initialisierung eines Uniform-Buffers mit einer vorgegebenen
    // Speichergröße:
    void Init_UniformBuffer(GLsizei BlockSize);

    // Binden des Uniform Buffers an den als Parameter übergebenen
    // Binding Point:
    // Hinweis:
    // Der Binding Point ist vergleichbar mit einem Texture Stage
    void Bind_UniformBuffer(long bindingPoint);

    // Binden eines Uniform-Blocks eines Shader-Programms. Notwendig, damit
    // man innerhalb eines Shaders auf die im Uniform Buffer gespeicherten
    // Daten zugreifen kann!! Als Binding Point wird der an die
    // Bind_UniformBuffer()-Methode übergebene Parameter verwendet!
    void Bind_UniformBlock(CGLSLShader* pShader);

    // Update aller Daten, die in einem Uniform Buffer gespeichert sind:
    void Update_Buffer(float* pData, long Size);

    // Update eines Teils der Daten, die in einem Uniform Buffer
    // gespeichert sind:
    void Update_Buffer(float* pData, long Offset, long Size);

    void Bind_Buffer(void);
    void Unbind_Buffer(void);

    void Update_Buffer_Without_NewBinding(float* pData, long Offset,
                                          long Size);
};


Kommen wir nun zur Implementierung der einzelnen CUniformBufferObject-Klassenmethoden. Konstruktor und Destruktor sind recht unspektakulär:

CUniformBufferObject::CUniformBufferObject()
{
    UniformBufferID    = 0;
    UniformBlockIndex  = 0;
    UniformBlockSize   = 0;
    UniformBufferIndex = 0;
    BindingPoint       = 0;
}

CUniformBufferObject::~CUniformBufferObject()
{
    glDeleteBuffers(1, &UniformBufferID);
}


Mithilfe der GetUniformBlockIndex()-Methode kann der Index eines in einem Shader definierten Uniform-Blocks ermittelt werden. Dieser Index ist notwendig, damit der Uniform-Buffer unter Berücksichtigung des Speicherbedarfs eines in einem Shader definierten Uniform-Blocks initialisiert werden kann. Des Weiteren wird der Index benötigt, um einen Uniform Block mit einem Binding Point zu verbinden.

void CUniformBufferObject::GetUniformBlockIndex(CGLSLShader* pShader,
                                                char* BlockName)
{
    UniformBlockIndex = glGetUniformBlockIndex(pShader->ShaderProgram,
                                               BlockName);

    if(GL_INVALID_INDEX == UniformBlockIndex)
        Add_To_Log("GL_INVALID_INDEX");
}


Ein Uniform-Buffer-Objekt lässt sich auf zwei Arten Initialisieren. In Variante 1 geben wir die Speichergröße des zu erzeugenden Buffers vor:

void CUniformBufferObject::Init_UniformBuffer(GLsizei BlockSize)
{
    BindingPoint = 0;

    glDeleteBuffers(1, &UniformBufferID);

    UniformBlockSize = BlockSize;

    glGenBuffers(1, &UniformBufferID);
    glBindBuffer(GL_UNIFORM_BUFFER, UniformBufferID);
    glBufferData(GL_UNIFORM_BUFFER, UniformBlockSize, 0, GL_DYNAMIC_DRAW);
    glBindBuffer(GL_UNIFORM_BUFFER, 0);

    UniformBufferIndex = g_UniformBufferIndex;
    g_UniformBufferIndex++;
}


In Variante 2 wird ein Uniform Buffer unter Berücksichtigung der Speichergröße eines in einem Shader definierten Uniform-Blocks initialisiert:

void CUniformBufferObject::Init_UniformBuffer(CGLSLShader* pShader)
{
    BindingPoint = 0;

    glGetActiveUniformBlockiv(pShader->ShaderProgram,
                              UniformBlockIndex,
                              GL_UNIFORM_BLOCK_DATA_SIZE,
                              &UniformBlockSize);

    Add_To_Log("GL_UNIFORM_BLOCK_DATA_SIZE", &UniformBlockSize);

    glDeleteBuffers(1, &UniformBufferID);

    glGenBuffers(1, &UniformBufferID);
    glBindBuffer(GL_UNIFORM_BUFFER, UniformBufferID);
    glBufferData(GL_UNIFORM_BUFFER, UniformBlockSize, 0, GL_DYNAMIC_DRAW);
    glBindBuffer(GL_UNIFORM_BUFFER, 0);

    UniformBufferIndex = g_UniformBufferIndex;
    g_UniformBufferIndex++;
}


Mithilfe der Bind_UniformBuffer()-Methode wird ein Uniform Buffer an den als Parameter übergebenen Binding Point gebunden. Der Binding Point ist vergleichbar mit einem Texture Stage:

void CUniformBufferObject::Bind_UniformBuffer(long bindingPoint)
{
    BindingPoint = bindingPoint;
    //Den Uniform Buffer mit dem Binding Point verbinden:
    glBindBufferBase(GL_UNIFORM_BUFFER, BindingPoint, UniformBufferID);
}


Mithilfe der Bind_UniformBlock()-Methode wird ein Uniform Block im als Parameter übergebenen Shader-Programm gebunden. Dies ist notwendig, damit man innerhalb eines Shaders auf die im Uniform Buffer gespeicherten Daten zugreifen kann. Als Binding Point wird der an die Bind_UniformBuffer()-Methode übergebene Parameter verwendet:

void CUniformBufferObject::Bind_UniformBlock(CGLSLShader* pShader)
{
    glUniformBlockBinding(pShader->ShaderProgram, UniformBlockIndex,
                          BindingPoint);
}


Es gibt Situationen, in denen sämtliche, in einem Uniform Buffer gespeicherte Daten aktualisiert werden müssen und Situationen, in denen lediglich vereinzelte Daten zu modifizieren sind. Die nachfolgende Methode ist für ein Update aller Daten gedacht:

void CUniformBufferObject::Update_Buffer(float* pData, long Size)
{
    glBindBuffer(GL_UNIFORM_BUFFER, UniformBufferID);
    glBufferData(GL_UNIFORM_BUFFER, Size, pData, GL_DYNAMIC_DRAW);
    glBindBuffer(GL_UNIFORM_BUFFER, 0);
}


Mithilfe der zweiten Update_Buffer()-Methode ist eine Aktualisierung eines Teils der im Buffer gespeicherten Daten möglich. Voraussetzung hierfür ist allerdings, dass man die Speicherposition der zu aktualisierenden Daten kennt. Hierfür muss der Offset (in Bytes) bezogen auf die erste Speicherposition bekannt sein.

void CUniformBufferObject::Update_Buffer(float* pData, long Offset,
                                         long Size)
{
    glBindBuffer(GL_UNIFORM_BUFFER, UniformBufferID);
    glBufferSubData(GL_UNIFORM_BUFFER, Offset, Size, pData);
    glBindBuffer(GL_UNIFORM_BUFFER, 0);
}


In bestimmten Situationen muss ein Uniform Buffer in mehreren Schritten aktualisiert werden. Für besagte Fälle ist die Verwendung der zuvor genannten Funktionen nicht ratsam, da bei jedem Funktionsaufruf der Buffer neu gebunden wird. Stattdessen bietet es sich an, getrennte Funktionen zum Binden und Update eines Buffers zu verwenden:

void CUniformBufferObject::Bind_Buffer(void)
{
    glBindBuffer(GL_UNIFORM_BUFFER, UniformBufferID);
}

void CUniformBufferObject::Unbind_Buffer(void)
{
    glBindBuffer(GL_UNIFORM_BUFFER, 0);
}

void CUniformBufferObject::Update_Buffer_Without_NewBinding(
                           float* pData, long Offset, long Size)
{
    glBufferSubData(GL_UNIFORM_BUFFER, Offset, Size, pData);
}


Wenden wir uns nun dem praktischen Einsatz der CUniformBufferObject-Klasse zu. Nachfolgend wird gezeigt, wie sich ein Uniform-Buffer-Objekt initialisieren und an einen Shader binden lässt:

LightingParameterUniformBuffer = new CUniformBufferObject;

LightingParameterUniformBuffer->GetUniformBlockIndex(pShader,
                                 "lightingParam");

// Buffer initialisieren (Variante 1):
LightingParameterUniformBuffer->Init_UniformBuffer(pShader);

// Buffer initialisieren (Variante 2):
//LightingParameterUniformBuffer->Init_UniformBuffer(32);

LightingParameterUniformBuffer->Bind_UniformBuffer(0);
LightingParameterUniformBuffer->Bind_UniformBlock(pShader);


Sollen nun die im Uniform Buffer gespeicherten Beleuchtungsparameter aktualisiert werden, so ist dies wie folgt möglich (konkret beziehen wir uns auf den anfangs im Beispiel-Shader definierten Uniform Block):

float LightingParam[8];

LightingParam[0] = -pLightDirection->x;
LightingParam[1] = -pLightDirection->y;
LightingParam[2] = -pLightDirection->z;
LightingParam[3] = BrightnessDistanceFactor;
LightingParam[4] = pLightColor->x;
LightingParam[5] = pLightColor->y;
LightingParam[6] = pLightColor->z;
LightingParam[7] = pLightColor->w;

LightingParameterUniformBuffer->Update_Buffer(LightingParam, 32);