Geometry Instancing Teil 2 – Verwendung von Uniform-Buffer-Objekten

Im ersten Artikel zum Thema Geometry Instancing wurden die zum Rendern benötigten Daten (Position in der Spielewelt aus Sicht der Kamera, Orientierung sowie Skalierung) der einzelnen Asteroiden-Instanzen in einem uniform-Array gespeichert. Aufgrund der limitierten Anzahl von uniform-Variablen, die von einem Shader verarbeitet werden können, kann jedoch nur eine verhältnismäßig geringe Zahl von Instanzen pro Draw Call gerendert werden.


Mithilfe der nachfolgenden OpenGL-Funktionen können Sie ermitteln, wie viele uniform-float-Komponenten maximal von einem Vertex, Fragment bzw. Geometry Shader verarbeitet werden können:

GLint x;

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

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

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


Hinweis:

Nehmen wir einmal an, GL_MAX_VERTEX_UNIFORM_COMPONENTS habe einen Wert von 1024. Jede 4x4-Matrix, die wir zum Speichern der für die Darstellung benötigten Daten verwenden, besteht ihrerseits aus 16 float-Variablen. Mit anderen Worten, uns stehen im Vertex Shader maximal 64 4x4-Matrizen vom Typ mat4 zur Verfügung – genügend Platz für lediglich 63 Instanzen sowie einer zusätzlichen Matrix für die Durchführung der Bildraum-Transformation (matViewProjection).

Die Verwendung von Uniform-Buffer-Objekten zum Speichern der Instanz-Daten ermöglicht uns die Darstellung einer deutlich größeren Anzahl von Instanzen pro Draw Call. In OpenGL-Tutorial 27 übernimmt die CAsteroidInstanceManager-Klasse das Handling besagter Uniform-Buffer-Objekte:

class CAsteroidInstanceManager
{
public:

    long NumInstances;
    D3DXMATRIXA16* WorldMatrix;

    long UsedInstances;

    CUniformBufferObject* InstanceTransformUniformBuffer;
    CUniformBufferObject* LightingParameterUniformBuffer;

    CAsteroidInstanceManager();
    ~CAsteroidInstanceManager();

    void Set_NumInstances(CGLSLShader* pShader, long numInstances);

    void Reset_All_Instances(void);

    void Set_WorldMatrix(D3DXMATRIXA16* pWorldMatrix);

    void Update_InstanceTransformUniformBuffer(void);
    void Update_LightingParamUniformBuffer(D3DXVECTOR3* pLightDirection,
                                           D3DXVECTOR4* pLightColor,
                                           float BrightnessDistanceFactor);
};


Insgesamt verwenden wir zwei Uniform Buffer, einen zum Speichern der Instanz-Daten (InstanceTransformUniformBuffer) sowie einen zweiten zum Speichern der Beleuchtungsparameter (LightingParameterUniformBuffer). Innerhalb der Set_NumInstances()-Methode werden beide Buffer initialisiert und an den zum Rendern verwendeten Shader gebunden:

void CAsteroidInstanceManager::Set_NumInstances(CGLSLShader* pShader,
                                                long numInstances)
{
    if(numInstances > 256)
        numInstances = 256;

    NumInstances = numInstances;
    UsedInstances = 0;

    SAFE_DELETE_ARRAY(WorldMatrix)
    SAFE_DELETE(InstanceTransformUniformBuffer)
    SAFE_DELETE(LightingParameterUniformBuffer)

    WorldMatrix = new D3DXMATRIXA16[NumInstances];

    InstanceTransformUniformBuffer = new CUniformBufferObject;
    LightingParameterUniformBuffer = new CUniformBufferObject;

    InstanceTransformUniformBuffer->GetUniformBlockIndex(pShader,
                                    "instanceData");
    InstanceTransformUniformBuffer->Init_UniformBuffer(pShader);
    InstanceTransformUniformBuffer->Bind_UniformBuffer(0);
    InstanceTransformUniformBuffer->Bind_UniformBlock(pShader);

    LightingParameterUniformBuffer->GetUniformBlockIndex(pShader,
                                    "lightingParam");
    LightingParameterUniformBuffer->Init_UniformBuffer(pShader);
    LightingParameterUniformBuffer->Bind_UniformBuffer(1);
    LightingParameterUniformBuffer->Bind_UniformBlock(pShader);
}


Für ein mögliches Update der Beleuchtungsparameter ist die Methode Update_LightingParamUniformBuffer() verantwortlich:

void CAsteroidInstanceManager::Update_LightingParamUniformBuffer(
                               D3DXVECTOR3* pLightDirection,
                               D3DXVECTOR4* pLightColor,
                               float BrightnessDistanceFactor)
{
    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);

    // Hinweis:
    // benötigter Speicherplatz für 8 float-Variablen: 32 Byte
}


Für ein Update der Beleuchtungsparameter benötigt die Update_LightingParamUniformBuffer()-Methode die Lichtrichtung, die Lichtfarbe sowie die Helligkeit der Lichtquelle:

D3DXVECTOR3 LightDirection (2.5f, 0.0f, 1.0f);
Normalize3DVector(&LightDirection, &LightDirection);

AsteroidInstanceManager->Update_LightingParamUniformBuffer(&LightDirection,
                         &D3DXVECTOR4(1.0f, 1.0f, 1.0f, 1.0f), 1.0);


Das Update der Instanz-Daten erfolgt innerhalb der Update_InstanceTransformUniformBuffer()-Methode:

void CAsteroidInstanceManager::Update_InstanceTransformUniformBuffer(void)
{
    InstanceTransformUniformBuffer->Update_Buffer(
        Build_ShaderMatrix4X4Array(WorldMatrix, UsedInstances),
        64*UsedInstances);

    // Hinweis:
    // benötigter Speicherplatz für eine 4x4-Matrix: 64 Byte
}


Nachdem im Hauptprogramm die Instanz-Daten aller sichtbaren Asteroiden aktualisiert worden sind, werden die aktualisierten Instanz-Daten mithilfe der Methode Update_InstanceTransformUniformBuffer() in den dafür vorgesehenen Uniform Buffer kopiert:

// Instanz-Daten zurücksetzen:
AsteroidInstanceManager->Reset_All_Instances();

// Alle sichtbaren Asteroiden durchgehen und die für die Darstellung
// benötigten Daten im CAsteroidInstanceManager-Objekt speichern:
for(i = 0; i < NumVisibleAsteroids; i++)
{
    j = PhysicalObjectDistanceData[i].ObjectNr;

    Asteroid[j].Update_InstanceParameter(AsteroidInstanceManager);
}

// aktualisierte Instanz-Daten in den Uniform Buffer kopieren:
AsteroidInstanceManager->Update_InstanceTransformUniformBuffer();


Dank der Verwendung der Uniform Buffer müssen im Zuge des Renderings nun deutlich weniger uniform-Daten zur Laufzeit an das verwendete Shader Programm gesandt werden:

void CSimpleAsteroidGraphics::Render(
     CAsteroidInstanceManager* pAsteroidInstanceManager)
{
    SimpleAsteroidShader->Use_Shader();

    SimpleAsteroidShader->Set_ShaderMatrix4X4(&g_ViewProjectionMatrix,
                                              "matViewProjection");

    SimpleAsteroidShader->Set_Texture(GL_TEXTURE0, 0, pSurfaceTexture,
                                      "AsteroidSurfaceTexture");

    SimpleAsteroidShader->Set_Texture(GL_TEXTURE1, 1, pNormalTexture,
                                      "AsteroidNormalTexture");

    pMeshVB_IB->Render_Mesh(SimpleAsteroidShader, 0,
                            pAsteroidInstanceManager->UsedInstances);

    SimpleAsteroidShader->Stop_Using_Shader();
}


Im Artikel OpenGL-Uniform-Buffer-Objekte sind wir bereits darauf eingegangen, wie im Fragment Shader auf die in einem Uniform Buffer gespeicherten Beleuchtungsparameter zugegriffen werden kann. Deutlich interessanter ist zweifelsohne die Verwendung des Uniform-Buffers im Rahmen des Geometry Instancings.

Es mag Sie verwundern, dass wir im zugehörigen Uniform Block (instanceData) die Instanz-Daten nicht in Form einzelner 4x4-Matrizen (mat4) verwalten sondern für jede Instanz auf vier vec4-Vektoren (matWorldArrayColumn) zurückgreifen. Für maximal 256 renderbare Instanzen benötigen wir daher ein Array von 1024 vec4-Vektoren.

Hinweis:
Der Speicherbedarf für ein Array von 1024 vec4-Vektoren unterscheidet sich im Uniform Block vom Speicherbedarf für ein Array von 256 mat4-Matrizen. Im Gegensatz zum 256-mat4-Array beansprucht ein 1024-vec4-Array 16384 Bytes an Speicher (1024 * 4 (float-Komponenten) * 4 (Byte pro float-Komponente) – genauso viel Speicher wie ein mat4x4(float)-Array im Hauptprogramm – wodurch eine 1:1 Übergabe der Matrixkomponenten vom Hauptprogramm an den Shader überhaupt erst ermöglicht wird.

Nachdem die Weltmatrix (matWorld) der zu rendernden Instanz aus den im Uniform Block (instanceData) gespeicherten Spaltenvektoren (matWorldArrayColumn) zusammengesetzt wurde, erfolgen die weiteren Rechenschritte genau wie beim Geometry Instancing ohne Verwendung eines Uniform-Buffers:

layout(shared) uniform instanceData
{
    vec4 matWorldArrayColumn[1024];   // 256(instances) * 4
};

uniform mat4 matViewProjection;

void main()
{
    // matWorld der zu rendernden Instanz aus den im Uniform Block
    // gespeicherten Spaltenvektoren zusammensetzen:
    int Index = 4*gl_InstanceID;

    mat4 matWorld = mat4(matWorldArrayColumn[Index],
                         matWorldArrayColumn[Index+1],
                         matWorldArrayColumn[Index+2],
                         matWorldArrayColumn[Index+3]);

    // alternativ:
    /*mat4 matWorld;
    matWorld[0] = matWorldArrayColumn[Index];
    matWorld[1] = matWorldArrayColumn[Index+1];
    matWorld[2] = matWorldArrayColumn[Index+2];
    matWorld[3] = matWorldArrayColumn[Index+3];*/

    // Skalierungsfaktor auslesen und aus der
    // Transformationsmatrix löschen:
    float Scale = matWorld[0][3];
    matWorld[0][3] = 0.0;

    // Skalieren der Vertexdaten:
    vec4 ScaledVertex = vec4(Scale*gs_Vertex.x, Scale*gs_Vertex.y,
                             Scale*gs_Vertex.z, gs_Vertex.w);

    gl_Position    = matViewProjection*matWorld*ScaledVertex;

    gs_TexCoord[0] = gs_MultiTexCoord0;

    mat3 matRotation;

    // Rotationsmatrix spaltenweise zusammensetzen:
    matRotation[0] = vec3(matWorld[0][0], matWorld[0][1], matWorld[0][2]);
    matRotation[1] = vec3(matWorld[1][0], matWorld[1][1], matWorld[1][2]);
    matRotation[2] = vec3(matWorld[2][0], matWorld[2][1], matWorld[2][2]);

    // Rotation des Modells berücksichtigen;
    gs_TexCoord[1].xyz = matRotation * gs_Normal;

    // Perpendicular2:
    gs_TexCoord[2].xyz = matRotation * gs_MultiTexCoord2.xyz;

    // Perpendicular1:
    gs_TexCoord[3].xyz = matRotation * gs_MultiTexCoord1.xyz;
}