Animation von 3D-Modellen Teil 07: Instanced Vertex Skinning mithilfe von Uniform-Buffer-Objekten

Im heutigen OpenGL-Artikel werden wir uns mit dem Einsatz von Uniform-Buffer-Objekten bei der Skelett-Animation (Vertex Skinning) von 3D-Modellen befassen. Da in einem Uniform Buffer weitaus mehr Animationsdaten gespeichert werden können als in einem einfachen uniform-Variablen-Array, lassen sich auf diese Weise deutlich realistischere Animationen auf Basis von entsprechend komplexeren Animationsskeletten verwirklichen.

Des Weiteren bietet es sich an, mithilfe eines zweiten Uniform-Buffers alle Instanzen eines 3D-Modells mit gleicher Animationspose in einem einzigen Draw Call zu rendern (Beispiele: Soldaten im Gleichschritt, Passanten, die Straße bevölkern sollen, eine Tierherde, ein Vogelschwarm, etc.).


Betrachten wir einmal den für das Geometry Instancing und Vertex Skinning zuständigen Vertex Shader:

#version 330
precision highp float;

. . .

// Der erste Uniform Block ermöglicht Zugriff auf die Instanzdaten:
layout(shared) uniform instanceData
{
    vec4 PositionAndTexureID[256];   // 256 Instanzen
    vec4 matViewProjectionColumn1;
    vec4 matViewProjectionColumn2;
    vec4 matViewProjectionColumn3;
    vec4 matViewProjectionColumn4;
};

// Der zweite Uniform Block ermöglicht Zugriff auf die Animationsdaten
// (für die Beschreibung der aktuellen Animationspose):
layout(shared) uniform animationData
{
    vec4 matJointTotalTransformArrayColumn[1024];   // 256 (joints) * 4
};

void main()
{
    int Index1 = int (gs_MultiTexCoord0.p);
    int Index2 = int(gs_MultiTexCoord0.q);

    Index1 *= 4;
    Index2 *= 4;

    // Animationsmatrix zusammensetzen:
    mat4 matJointTotalTransform = mat4(
         matJointTotalTransformArrayColumn[Index1],
         matJointTotalTransformArrayColumn[Index1+1],
         matJointTotalTransformArrayColumn[Index1+2],
         matJointTotalTransformArrayColumn[Index1+3]);

    // Transformation der Vertexdaten in die korrekte Animationspose
    // im Model Space:
    vec4 AnimatedPos = matJointTotalTransform * gs_Vertex;

    // Position im Kameraraum festlegen:
    AnimatedPos.xyz += PositionAndTexureID[gl_InstanceID].xyz;

    mat4 matViewProjection = mat4(matViewProjectionColumn1,
                                  matViewProjectionColumn2,
                                  matViewProjectionColumn3,
                                  matViewProjectionColumn4);

    // Mithilfe ViewProjection-Matrix kann das Modell korrekt in den
    // Kamera-Raum tranformiert und schließlich an der korrekten
    // Bildschirmposition (Bildraum-(Projektions)-Transformation)
    // dargestellt werden:

    gl_Position = matViewProjection * AnimatedPos;

    gs_TexCoord[0] = gs_MultiTexCoord0;

    // Hinweis:
    // An dieser Stelle könnte man zusätzlich das im Fragment Shader zu
    // verwendende Texture Set (Surface Map, Normal Map,
    // Light Map u. Specular Map)festlegen, falls mehrere Sets zur Auswahl
    // stünden!
    // PositionAndTexureID[gl_InstanceID].w := Texturindex
    // gs_TexCoord[0] = vec4(gs_MultiTexCoord0.xyz,
    //                       PositionAndTexureID[gl_InstanceID].w);

    // Matrix für die korrekte Transformation der Vertexnormalen
    // zusammensetzen (wichtig für die Beleuchtung im Fragment Shader):
    mat3 JointTotalRotationMatrix;
    JointTotalRotationMatrix[0] = matJointTotalTransformArrayColumn[Index2].xyz;
    JointTotalRotationMatrix[1] = matJointTotalTransformArrayColumn[Index2+1].xyz;
    JointTotalRotationMatrix[2] = matJointTotalTransformArrayColumn[Index2+2].xyz;

    gs_TexCoord[1].xyz = JointTotalRotationMatrix * gs_Normal;

    // Perpendicular2 (wichtig für das Normal Mapping):
    gs_TexCoord[2].xyz = JointTotalRotationMatrix * gs_MultiTexCoord2.xyz;

    // Perpendicular1 (wichtig für das Normal Mapping):
    gs_TexCoord[3].xyz = JointTotalRotationMatrix * gs_MultiTexCoord1.xyz;
}


In OpenGL-Tutorial 29 übernimmt die CAnimatedMeshInstanceManager-Klasse das Handling der verwendeten Uniform-Buffer-Objekte:

class CAnimatedMeshInstanceManager
{
public:

    long NumInstances;

    D3DXVECTOR4* PositionAndTexureID;

    long UsedInstances;

    CUniformBufferObject* InstanceTransformUniformBuffer;
    CUniformBufferObject* AnimationDataUniformBuffer;
    CUniformBufferObject* LightingParameterUniformBuffer;

    CAnimatedMeshInstanceManager();
    ~CAnimatedMeshInstanceManager();

    void Set_NumInstances(CGLSLShader* pShader, long numInstances);

    void Reset_All_Instances(void);

    void Set_WorldSpacePosition(D3DXVECTOR3* pPosition);

    void Update_AnimationDataUniformBuffer(
                D3DXMATRIXA16* pJointTotalTransformMatrixArray,
                long NumJoints);

    void Update_LightingParamUniformBuffer(D3DXVECTOR3* pLightDirection,
                                           D3DXVECTOR4* pLightColor,
                                           float AmbientValue);

    void Update_InstanceTransformUniformBuffer(void);
};


Insgesamt verwenden wir drei Uniform Buffer, einen zum Speichern der Instanzdaten (InstanceTransformUniformBuffer), einen zweiten zum Speichern der Animationsdaten (AnimationDataUniformBuffer) sowie einen dritten zum Speichern der Beleuchtungsparameter (LightingParameterUniformBuffer). Innerhalb der Set_NumInstances()-Methode werden alle drei Buffer initialisiert und an den zum Rendern verwendeten Shader gebunden:


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

    NumInstances = numInstances;
    UsedInstances = 0;

    SAFE_DELETE_ARRAY(PositionAndTexureID)
    SAFE_DELETE(InstanceTransformUniformBuffer)
    SAFE_DELETE(LightingParameterUniformBuffer)
    SAFE_DELETE(AnimationDataUniformBuffer)

    PositionAndTexureID = new D3DXVECTOR4[NumInstances];

    InstanceTransformUniformBuffer = new CUniformBufferObject;
    AnimationDataUniformBuffer = new CUniformBufferObject;
    LightingParameterUniformBuffer = new CUniformBufferObject;

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

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

    AnimationDataUniformBuffer->GetUniformBlockIndex(pShader,
                                "animationData");

    AnimationDataUniformBuffer->Init_UniformBuffer(pShader);
    AnimationDataUniformBuffer->Bind_UniformBuffer(1);
    AnimationDataUniformBuffer->Bind_UniformBlock(pShader);

    InstanceTransformUniformBuffer->GetUniformBlockIndex(pShader,
                                    "instanceData");

    InstanceTransformUniformBuffer->Init_UniformBuffer(pShader);
    InstanceTransformUniformBuffer->Bind_UniformBuffer(2);
    InstanceTransformUniformBuffer->Bind_UniformBlock(pShader);
}


Mithilfe der Set_WorldSpacePosition()-Methode lässt sich die Position einer Instanz in der Spielewelt festlegen:

void CAnimatedMeshInstanceManager::Set_WorldSpacePosition(
                                   D3DXVECTOR3* pPosition)
{
    D3DXVECTOR3 CameraSpacePosition = *pPosition - g_CameraPosition;

    PositionAndTexureID[UsedInstances] = D3DXVECTOR4(CameraSpacePosition.x,
                                                     CameraSpacePosition.y,
                                                     CameraSpacePosition.z,
                                                     1.0);

    UsedInstances++;
}


Der nachfolgende Code-Ausschnitt zeigt, wie in OpenGL-Tutorial 29 die Positionen der animierten Instanzen festgelegt werden. Bitte beachten Sie, dass man bei dieser Festlegung immer auch die aktuelle Animationspose berücksichtigen muss, da sich im Verlauf einer Animation unter Umständen die Lage des Körperschwerpunkts verändern kann. In diesem Zusammenhang beschreibt InterpolatedBodyPosition die Position des Schwerpunkts im Model Space:

// Position der zu rendernden Instanzen festlegen:
SkeletalAnimatedBody_WorldSpacePosition = D3DXVECTOR3(-3.0f, 0.0f, 5.0f) +
WalkingSequence->InterpolatedBodyPosition;

AnimatedMeshInstanceManager->Set_WorldSpacePosition(
                             &SkeletalAnimatedBody_WorldSpacePosition);

SkeletalAnimatedBody_WorldSpacePosition = D3DXVECTOR3(-1.0f, 0.0f, 5.0f) +
WalkingSequence->InterpolatedBodyPosition;

AnimatedMeshInstanceManager->Set_WorldSpacePosition(

                             &SkeletalAnimatedBody_WorldSpacePosition);

. . .


Nachdem im Hauptprogramm die Instanzdaten aller animierten Figuren aktualisiert worden sind, werden diese mithilfe der Methode Update_InstanceTransformUniformBuffer() in den dafür vorgesehenen Uniform Buffer kopiert:

void CAnimatedMeshInstanceManager::Update_InstanceTransformUniformBuffer(void)
{
    InstanceTransformUniformBuffer->Bind_Buffer();

    InstanceTransformUniformBuffer->Update_Buffer_Without_NewBinding(
            Build_ShaderVector4Array(PositionAndTexureID, UsedInstances),
            0, 16*UsedInstances);

    // In die letzte freie Stelle im Uniform-Buffer (nach den Positionen
    // aller 256 möglichen Instanzen) wird die Viewprojection-Matrix
    // kopiert:
   
InstanceTransformUniformBuffer->Update_Buffer_Without_NewBinding(
            Build_ShaderMatrix4X4(&g_ViewProjectionMatrix), 16*256, 64);

    InstanceTransformUniformBuffer->Unbind_Buffer();
}


Nach dem gleichen Prinzip werden auch die Animationsdaten für die Beschreibung der aktuellen Animationspose mithilfe der Update_AnimationDataUniformBuffer()-Methode in den dafür vorgesehenen Uniform Buffer kopiert:

void CAnimatedMeshInstanceManager::Update_AnimationDataUniformBuffer(
                        D3DXMATRIXA16* pJointTotalTransformMatrixArray,
                        long NumJoints)
{
    AnimationDataUniformBuffer->Update_Buffer(
    Build_ShaderMatrix4X4Array(pJointTotalTransformMatrixArray, NumJoints),
    64*NumJoints);
}


Dank der verwendeten Uniform-Buffer-Objekte müssen beim Aufruf der Rendermethode bis auf die zu verwendenden Texturen keine weiteren Daten an das zuständige Shader Programm geschickt werden:

void CModel::Render_With_Actual_NM_Shader(CGLSLShader* pShader,
                                          long LODStep, long NumInstances)
{
    pShader->Set_Texture(GL_TEXTURE0, 0, pSurfaceTexture, "SurfaceTexture");
    pShader->Set_Texture(GL_TEXTURE1, 1, pNormalTexture, "NormalTexture");

    pMeshVB_IB->Render_Mesh(pShader, LODStep, NumInstances);
}