Geometry Instancing Teil 1

Durch den Einsatz von Geometry Instancing ist es möglich, eine große Anzahl von 3D-Objekten (Instanzen) gleicher Geometrie mit nur einem einzigen Draw Call (Renderaufruf) zu rendern.
Doch warum genau führt das Geometrie Instancing zu so einem großen Performanceschub? Worin besteht überhaupt der Vorteil, mehrere Instanzen auf einmal zu rendern?
In diesem Zusammenhang müssen Sie wissen, dass auf modernen Grafikkarten die Darstellung eines einfachen 3D-Modells (mit mehreren hundert bis hin zu wenigen tausend Dreiecksflächen) inklusive aller erforderlichen Vertex- und Fragment-Shader-Berechnungen deutlich weniger Zeit beansprucht als die Verarbeitung und Weiterleitung (CPU => GPU) eines Draw Calls.
Da sich mit zunehmender Leistungsfähigkeit der Grafikkarten die für die Darstellung eines 3D-Modells benötigte Zeit immer weiter verringert, während gleichzeitig die für einen Draw Call erforderliche Zeit mehr oder weniger unverändert bleibt, vergrößert sich mit jeder neuen Generation von Grafikkarten der Performancegewinn, der sich durch Geometry Instancing erzielen lässt. Darüber hinaus führt das Instancing auch bei zunehmend komplexeren 3D-Modellen zu einem messbaren Geschwindigkeitszuwachs.


Geometry Instancing eignet sich gleichsam für die Darstellung einer großen Zahl von mehr oder weniger ortsgebundenen (statischen) Objekten (z.B. Vegetation oder Terrain-Tiles) als auch für die Darstellung von von interaktiven Partikelsystemen, Menschen, Tieren, Asteroiden, Raumschiffen, Fahrzeugen, usw.

Hinweis:
Bei interaktiven Partikelsystemen werden mögliche Interaktionen zwischen einzelnen Partikeln und der Spielewelt normalerweise von der CPU berechnet. Im Anschluss daran ist die Darstellung sämtlicher Partikel dank Geometry Instancing mit einigen wenigen Draw Calls möglich.

In unseren bisher veröffentlichten OpenGL-Tutorials (25, 26, 27) nutzen wir Geometry Instancing für die Darstellung eines Asteroidenfeldes. Zum Rendern der Asteroiden verwenden wir nun anstelle von glDrawElements() die glDrawElementsInstanced()-Methode, der wir als zusätzlichen Parameter die Anzahl der zu rendernden Asteroiden-Instanzen übergeben müssen:

glBindBuffer(GL_ARRAY_BUFFER, MeshVBId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, MeshIndexData[LODStep].MeshIBId);

pShader->Set_VertexAttributes();

glDrawElementsInstanced(GL_TRIANGLES, MeshIndexData[LODStep].NumIndices,
                        GL_UNSIGNED_SHORT, NULL, NumInstances);

pShader->Reset_VertexAttributes();

Da wir die Asteroiden nun nicht mehr einzeln rendern, müssen wir vor dem Aufruf der glDrawElementsInstanced()-Methode alle für die Darstellung der einzelnen Asteroiden-Instanzen benötigten Daten (Position in der Spielewelt aus Sicht der Kamera, Orientierung sowie Skalierung) an den zu verwendenden Shader übergeben. Soll nur eine kleine Anzahl von Instanzen gerendert werden, können die Daten in einem einfachen uniform-mat4-Array gespeichert werden. Aktualisiert und verwaltet werden diese Instanz-Daten mithilfe der CAsteroidInstanceManager-Klasse.

class CAsteroidInstanceManager
{
public:

    long NumInstances;

    // In der WorldMatrix werden Position, Orientierung
    // und Skalierung einer Instanz gespeichert:
    D3DXMATRIXA16* WorldMatrix;

    // Hinweis:
    // In unseren OpenGL-Tutorials verwenden wir lediglich einen einzelnen
    // Skalierungsfaktor. Wir hätten in der WorldMatrix noch genügend
    // Platz, um zusätzlich einen Index für die zu verwendende Textur zu
    // speichern!

    long UsedInstances;

    CAsteroidInstanceManager();
    ~CAsteroidInstanceManager();

    // Festlegen, wie viele Instanzen maximal gerendert werden sollen:
    void Set_NumInstances(long numInstances);

    // Bevor die Daten der einzelnen Instanzen für das aktuelle Frame
    // gespeichert werden können, muss zunächst die Reset_All_Instances()-
    // Methode aufgerufen werden:
    void Reset_All_Instances(void);

    // Für jede zu rendernde Instanz müssen die für die Darstellung
    // benötigten Daten in Form einer 4x4-Matrix (D3DXMATRIXA16) an die
    // Set_WorldMatrix()-Methode übergeben werden:
    void Set_WorldMatrix(D3DXMATRIXA16* pWorldMatrix);
};

Die Implementierungen der einzelnen CAsteroidInstanceManager-Methoden bestehen allesamt aus nur wenigen Codezeilen und sollten sich daher entsprechend einfach nachvollziehen lassen:

CAsteroidInstanceManager::CAsteroidInstanceManager()
{
    WorldMatrix = NULL;

    NumInstances = 0;
    UsedInstances = 0;
}

CAsteroidInstanceManager::~CAsteroidInstanceManager()
{
    SAFE_DELETE_ARRAY(WorldMatrix)
}

void CAsteroidInstanceManager::Set_NumInstances(long numInstances)
{
    NumInstances = numInstances;
    UsedInstances = 0;

    SAFE_DELETE_ARRAY(WorldMatrix)

    WorldMatrix = new D3DXMATRIXA16[NumInstances];
}

void CAsteroidInstanceManager::Reset_All_Instances(void)
{
    UsedInstances = 0;
}

void CAsteroidInstanceManager::Set_WorldMatrix(D3DXMATRIXA16* pWorldMatrix)
{
    // Die neuen Instanz-Daten in einem freien Array-Element speichern:
    WorldMatrix[UsedInstances] = *pWorldMatrix;
    UsedInstances++;
}

Aus dem Hauptprogramm heraus werden die Instanz-Daten wie folgt aktualisiert:

// 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);
}

Bevor die Instanz-Daten jedoch aktualisiert werden können, müssen die benötigten Daten zunächst einmal zu einer 4x4-Matrix (D3DXMATRIXA16) zusammengefasst werden. Verantworlich hierfür ist die CAsteroid-Methode Update_InstanceParameter():

void CAsteroid::Update_InstanceParameter(
                CAsteroidInstanceManager* pAsteroidInstanceManager)
{
    D3DXMATRIXA16 RotationTranslationMatrix = OrientationMatrix;

    RotationTranslationMatrix._41 = CameraSpacePosition.x;
    RotationTranslationMatrix._42 = CameraSpacePosition.y;
    RotationTranslationMatrix._43 = CameraSpacePosition.z;

    // Hinweis:
    // Die Matrixelemente _14, _24 und _34 bleiben ungenutzt und lassen
    // sich zum Speichern zusätzlicher Daten verwenden. Wir nutzen
    // lediglich eines der Elemente zum Speichern eines einzelnen
    // Skalierungsfaktors.
    // Wir hätten in der WorldMatrix noch genügend Platz, um zusätzlich
    // einen Index für die zu verwendende Textur zu speichern!

    RotationTranslationMatrix._14 = ScaleFactor;

    // Aktualisieren der Instanz-Daten:
    pAsteroidInstanceManager->Set_WorldMatrix(&RotationTranslationMatrix);
}

Im Zuge des Renderings müssen die für die Darstellung der Asteroiden-Instanzen benötigten Daten an den Shader übergeben werden:

void CSimpleAsteroidGraphics::Render(
     CAsteroidInstanceManager* pAsteroidInstanceManager,
     D3DXVECTOR3* pLightDirection, D3DXVECTOR4* pLightColor,
     float BrightnessDistanceFactor)
{
    SimpleAsteroidShader->Use_Shader();

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

    // Übergabe aller für die Darstellung der Asteroiden-Instanzen
    // benötigten Daten an den Shader:
    SimpleAsteroidShader->Set_ShaderMatrix4X4Array(
                          pAsteroidInstanceManager->WorldMatrix,
                          pAsteroidInstanceManager->UsedInstances,
                          "matWorldArray");

    SimpleAsteroidShader->Set_Texture(GL_TEXTURE0, 0, pSurfaceTexture,
                                      "AsteroidSurfaceTexture");
    SimpleAsteroidShader->Set_Texture(GL_TEXTURE1, 1, pNormalTexture,
                                      "AsteroidNormalTexture");

    SimpleAsteroidShader->Set_ShaderFloatVector4(pLightColor,
                                                 "LightColor");

    SimpleAsteroidShader->Set_ShaderFloatVector3(
                          &D3DXVECTOR3(-pLightDirection->x,
                                       -pLightDirection->y,
                                       -pLightDirection->z),
                          "negLightDir");

    SimpleAsteroidShader->Set_ShaderFloatValue(BrightnessDistanceFactor,
                                               "BrightnessDistanceFactor");

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

    SimpleAsteroidShader->Stop_Using_Shader();
}

Im Vertex Shader werden die zu rendernden Asteroiden-Instanzen mithilfe des Build-In-Shader-Attributs gl_InstanceID identifiziert. gl_InstanceID wird zunächst mit 0 initialisiert und für jede Instanz um den Wert 1 inkremetiert. Wir benötigen dieses Shader-Attribut, um die für die Darstellung der Instanzen benötigten Daten aus dem Instanz-Daten-Array (matWorldArray) auszulesen.

// Transformationsmatrizen:
uniform mat4 matViewProjection;

// Instanz-Daten-Array:
uniform mat4 matWorldArray[63];

void main()
{
    // Die für die Darstellung der zu rendernden Instanz
    // benötigten Daten aus dem Instanz-Daten-Array auslesen:
    mat4 matWorld = matWorldArray[gl_InstanceID];

    // 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, wichtig für das Normal Mapping
    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;
}