Texture Buffer Geometry Instancing

Im Rahmen der Entwicklung eines Prototypen für ein Weltraum-Strategiespiel galt es die Frage zu klären, wie sich eine prozedural erzeugte Galaxie mit tausenden Sternen und 3D-Nebelwolken mit nur wenigen Draw Calls darstellen lässt. Nach einer längeren Testphase mit Uniform-Buffer-basiertem Geometry Instancing fiel die Wahl letztlich auf den Einsatz von Texture-Buffer-Objekten, da sich auf diese Weise alle Sterne und alle Nebelwolken mit jeweils einem einzigen Draw Call rendern lassen.


Verantwortlich für die Initialisierung der für das Geometry Instancing benötigten Texture-Buffer-Objekte sind die Set_NumSunsMax()-Methode der CGalaxyGraphics-Klasse sowie die Set_NumNebulaObjectsMax()-Methode der C3DNebulaSystem-Klasse. Bitte haben Sie dafür Verständnis, dass wir im Rahmen dieses Artikels unser Augenmerk lediglich auf die Darstellung der Sterne richten werden:

void CGalaxyGraphics::Set_NumSunsMax(long numSunsMax)
{
SAFE_DELETE_ARRAY(InstanceWorldViewProjectionMatrix)
SAFE_DELETE_ARRAY(WorldSpacePosition)
SAFE_DELETE_ARRAY(SunScale)
SAFE_DELETE_ARRAY(SunTextureID)
SAFE_DELETE_ARRAY(CameraSpacePosition)
SAFE_DELETE_ARRAY(IDOfSunsNearTheCamera)

SAFE_DELETE(InstanceDataArray)
SAFE_DELETE(SunInstancesTBO)

IDOfSunNearTheCamera = 0;
MaxLocalGroupDistanceSq = 0.0f;
NumOfSunsInsideLocalGroup = 0;

NumSunsMax = numSunsMax;
NumSunsDefined = 0;
NumSunsVisible = 0;

InstanceWorldViewProjectionMatrix = new D3DXMATRIXA16[NumSunsMax];
WorldSpacePosition                = new D3DXVECTOR3[NumSunsMax];
SunScale                          = new float[NumSunsMax];
SunTextureID                      = new float[NumSunsMax];
CameraSpacePosition               = new D3DXVECTOR3[NumSunsMax];
IDOfSunsNearTheCamera             = new long[NumSunsMax];

SunInstancesTBO = new CTextureBufferObject;

// Pro Sonne werden 16 float-Parameter im Texture Buffer
// gespeichert:
SunInstancesTBO->Init_TextureBuffer(4*NumSunsMax, 4);

InstanceDataArray = new float[16*NumSunsMax];

}


Bei Programmstart werden zunächst alle Sterne bzw. Nebelwolken im Weltraum positioniert:

void CGalaxyGraphics::Init_Sun(D3DXVECTOR3* pWorldSpacePosition,
                               float sunScale, long sunTextureID)
{
if(NumSunsDefined >= NumSunsMax)
    return;

WorldSpacePosition[NumSunsDefined] = *pWorldSpacePosition;
SunScale[NumSunsDefined] = sunScale;
SunTextureID[NumSunsDefined] = sunTextureID;

NumSunsDefined++;
}


Die Update_InstanceWorldViewProjectionMatrices()-Methoden der CGalaxyGraphics-Klasse sind für die Überprüfung der Sichtbarkeit der einzelnen Sterne sowie für die Berechnung der für die korrekte Darstellung benötigten World-View-Projection-Matrizen verantwortlich:

void CGalaxyGraphics::Update_InstanceWorldViewProjectionMatrices(
     long IdOfCenteredSun, float CenteredSunScaleMultiplier,
     D3DXVECTOR3* pCameraPosition, D3DXVECTOR3* pCameraViewDirection,
     D3DXMATRIXA16* pViewProjectionMatrix, float maxVisibilityDistanceSq)
{
// Diese Methode kommt immer dann zum Einsatz, wenn sich der Spieler
// (und mit ihm die Kamera) innerhalb eines Sonnensystems (IdOfCenteredSun)
// befindet! Alle weiteren Sterne liegen im Hintergrund.

float angle = 0.0f;

IDOfSunNearTheCamera = 0;

NumOfSunsInsideLocalGroup = 0;

float minDistanceSq = 100000000000000.0f;
float DistanceSq;

NumSunsVisible = 0;

float modifiedScale;

for(long i = 0; i < NumSunsDefined; i++)
{
if(IdOfCenteredSun == i)
    // Position des Zentralgestirns relativ zur Kamera im Sonnensystem,
    // in dem sich der Spieler momentan befindet:
    CameraSpacePosition[i] = WorldSpacePosition[i] - *pCameraPosition;
else
    // Der Abstand aller Hintergrundsterne ist unabhängig von der
    // aktuellen Position der Kamera:
    CameraSpacePosition[i] = WorldSpacePosition[i] –
                             WorldSpacePosition[IdOfCenteredSun];

// Sichtbarkeitstest:
DistanceSq = D3DXVec3LengthSq(&CameraSpacePosition[i]);

if(DistanceSq > maxVisibilityDistanceSq)
    continue;

if(D3DXVec3Dot(&CameraSpacePosition[i], pCameraViewDirection) < 0.0f)
    continue;

// Sichtbarkeitstest abgeschlossen

// Speichern der Indizes aller Sterne in der näheren Umgebung
// (wichtig sowohl für die Anzeige der Sternennamen als auch für
//  die Darstellung von Lens-Flare-Effekten!).
if(DistanceSq < MaxLocalGroupDistanceSq)
{
    IDOfSunsNearTheCamera[NumOfSunsInsideLocalGroup] = i;
    NumOfSunsInsideLocalGroup++;
}

if(DistanceSq < minDistanceSq)
{
    minDistanceSq = DistanceSq;
    IDOfSunNearTheCamera = i;
}

// Berechnung der World-View-Projection-Matrizen:
if(IdOfCenteredSun == i)
{
    modifiedScale = CenteredSunScaleMultiplier*SunScale[i];

    Positioning_2DObject_rotated(
        &InstanceWorldViewProjectionMatrix[NumSunsVisible],
        &CameraSpacePosition[i], modifiedScale, angle);
}
else
    Positioning_2DObject_rotated(
        &InstanceWorldViewProjectionMatrix[NumSunsVisible],
        &CameraSpacePosition[i], SunScale[i], angle);

InstanceWorldViewProjectionMatrix[NumSunsVisible] =
    InstanceWorldViewProjectionMatrix[NumSunsVisible]*
    (*pViewProjectionMatrix);

// Null-Element zum Speichern des Textur-Index zweckentfremden:
InstanceWorldViewProjectionMatrix[NumSunsVisible]._14 = SunTextureID[i];

NumSunsVisible++;
}
}


Im zweiten Schritt müssen die für die Darstellung der Instanzen benötigten Daten – die Indizes der zu verwendenden Texturen sowie die Elemente der zuvor berechneten World-View-Projection-Matrizen – mithilfe der Update_SunInstancesTBO()-Methode in den Texture Buffer kopiert werden:

void CGalaxyGraphics::Update_SunInstancesTBO(void)
{
Build_ShaderMatrix4X4Array(InstanceDataArray,
InstanceWorldViewProjectionMatrix, NumSunsVisible);

// Pro Instance/Matrix 64 Byte (4x4=16 Elemente mit je 32 Bit bzw. 4 Byte):
SunInstancesTBO->Update_Buffer(InstanceDataArray, 0, 64*NumSunsVisible);
}


Im letzten Schritt können die sichtbaren Sterne schließlich unter Verwendung der beiden CGalaxyGraphics-Methoden Render_Galaxy() sowie Render_Galaxy_AsBackground() gerendert werden:

void CGalaxyGraphics::Render_Galaxy_AsBackground(void)
{
glDisable(GL_DEPTH_TEST);

glEnable(GL_BLEND);

glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);


glBlendFunc(GL_ONE,GL_ONE_MINUS_SRC_COLOR);

BillboardBackgroundShader->Use_Shader();


BillboardBackgroundShader->Set_TextureBuffer(0,
SunInstancesTBO->Texture,  "SunInstancesTextureBuffer");

BillboardBackgroundShader->Set_TextureArray(1,
pSunTextureArray, g_MipMappingSamplerID, "SunTextureArray");


glBindBuffer( GL_ARRAY_BUFFER, VertexBufferId);
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, IndexBufferId);

BillboardBackgroundShader->Set_VertexAttributes();

glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL,
                        NumSunsVisible);

BillboardBackgroundShader->Stop_Using_Shader();

glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glCullFace(GL_BACK);
}


Der Zugriff auf einen Texture Buffer innerhalb eines Shader-Programms sowie die Verwendung von Texture Arrays beim Geometry Instancing wurden bereits in den vorangegangenen Beiträgen thematisiert. Für die Sternen- und Nebeldarstellung werden nun beide Techniken wie folgt miteinander kombiniert:

Vertex Shader:

uniform samplerBuffer SunInstancesTextureBuffer;


void main()
{
int Index = 4*gl_InstanceID;

mat4 matWorldViewProjection  = mat4(texelFetch(SunInstancesTextureBuffer,
                                    Index++),
                                    texelFetch(SunInstancesTextureBuffer,
                                    Index++),
                                    texelFetch(SunInstancesTextureBuffer,
                                    Index++),
                                    texelFetch(SunInstancesTextureBuffer,
                                    Index));

// Texture-ID ist im Matrixelement matWorldViewProjection[0][3]
// gespeichert

gs_TexCoord[0] = vec4(gs_MultiTexCoord0.xy, matWorldViewProjection[0][3],
                      1.0);

// Texture-ID löschen, damit die Transformation der Vertexdaten mithilfe
// matWorldViewProjection-Matrix wieder korrekt funktioniert:
matWorldViewProjection[0][3] = 0.0;

gl_Position = matWorldViewProjection*gs_Vertex;
}


Fragment Shader:

uniform sampler2DArray SunTextureArray;


void main()
{
// Hinweise:
// gs_TexCoord[0].z:  Index der zu verwendenden Textur (Texture-ID)
// gs_TexCoord[0].xy: die normalen Texturkoordinaten

vec4 SunColor = texture(SunTextureArray, gs_TexCoord[0].xyz);


if(SunColor.a < 0.01)
    discard;

 gs_FragColor = SunColor;
}