Animation von 3D-Modellen Teil 04: Anbinden eines 3D-Modells an ein Animations-Skelett

Im heutigen Artikel befassen wir uns damit, wie man ein 3D-Modell mit einem Animations-Skelett verbinden kann – kurzum, wir versehen unser Animations-Skelett mit einer Haut (Skin). Unser Augenmerk richtet sich dabei auf die Entwicklung eines Verfahrens, dass die Anbindung der Modell-Vertices an die optimalen Gelenke automatisch vornimmt.
Zunächst jedoch müssen wir uns über das zu verwendende Vertexformat Gedanken machen. Bisher haben wir für die Darstellung von zu beleuchtenden Oberflächen (via Normal bzw. Parallax Mapping) folgendes Format verwendet:

struct CTexturedVertexWithNormal_NM
{
    // Über
tw könnte man bsp. den Index der zu verwendenden Textur
    // festlegen:
    float tu, tv, tw;

    // die nachfolgenden 9 Variablen sind die Elemente der
    // Transformationsmatrix (Texture Space -> Model Space):
    float Perpendicular1X, Perpendicular1Y, Perpendicular1Z;
    float Perpendicular2X, Perpendicular2Y, Perpendicular2Z;
    float NormalX, NormalY, NormalZ;

    float PosX, PosY, PosZ;
};

Im Prinzip könnten wir dieses Format auch für unsere animierten 3D-Modelle verwenden und im Verlauf der Vertex-Transformationen mithilfe des Parameters tw auf die zum zugeordneten Gelenk zugehörige Transformationsmatrix zugreifen. Stattdessen werden wir jedoch auf ein um einen zusätzlichen Parameter erweitertes Format zurückgreifen, da uns dies die Möglichkeit eröffnet, für die Transformation von Normale und Position separate Matrizen, bzw. mehrere Texturen zu verwenden:

struct CTexturedAnimatedVertexWithNormal_NM
{
    // Über die Parameter
tw und ta wird der Vertex an
    // einen Joint gebunden. tw speichert den Index der
    // zu verwendenden Matrix für die Transformation
    // der Position,
ta speichert den Index der zu verwendenden
    // Matrix für die Transformation der Normale. Alternativ könnte
    //
ta auch den Index der zu verwendenden Textur speichern:
    float tu, tv, tw, ta;

    // die nachfolgenden 9 Variablen sind die Elemente der
    // Transformationsmatrix (Texture Space -> Model Space):
    float Perpendicular1X, Perpendicular1Y, Perpendicular1Z;
    float Perpendicular2X, Perpendicular2Y, Perpendicular2Z;
    float NormalX, NormalY, NormalZ;

    float PosX, PosY, PosZ;
};

Hinweis:
Im Zuge der Skelett-Animation ist es nicht unüblich, einem Vertex mehrere Gelenke zuzuweisen und über Gewichtungsfaktoren festzulegen, wie stark ein Vertex durch die Bewegung des jeweiligen Gelenks beeinflusst wird. Man spricht hierbei vom sogenannten indizierten Vertex Blending. Alternativ dazu kann man natürlich auch die Anzahl der Gelenke erhöhen, was ebenfalls zu schöneren Animationen führt.


Die Zuordnung von Vertex und Gelenk erfolgt in maximal vier Schritten:

  1. Index desjenigen Gelenks suchen und speichern, das den kleinsten quadratischen Abstand vom betrachteten Vertex aufweist.
  2. Überprüfen, zu welchen Knochen der betrachtete Vertex gehören könnte. Quadratische Abstände zwischen dem betrachteten Vertex und den Endgelenken bestimmen und den Index desjenigen Endgelenks mit dem kleinsten quadrat. Abstand speichern.
  3. Falls die Zuordnung im zweiten Schritt noch nicht erfolgreich war, alle Knochen durchgehen und überprüfen, ob das im ersten Schritt gefundene Gelenk ein Drehgelenk ist. Sollte dies der Fall sein, den Index des Endgelenks des betreffenden Knochens speichern.
  4. Sollte die Zuordnung immer noch nicht erfolgreich sein, dann einfach den Index des im ersten Schritt gefundenen Gelenks verwenden.

Alle Vertices sollten wenn möglich einem Endgelenk zugeordnet werden. Bedenken Sie in diesem Zusammenhang, dass sich bei der Drehung eines Knochens um dessen Drehgelenk nur die Position des Endgelenks verändert. Damit die Bewegung des Knochens auf die zum Knochen zugehörigen Vertices übertragen werden kann, müssen diese Vertices dem Endgelenk zugeordnet werden:











Hier nun der zugehörige Soure Code:

// Vertices an die Gelenke anbinden:

float distanceSq, minDistanceSq1, minDistanceSq2;
long tempVertexID1, tempVertexID2;

for(i = 0; i < NumMeshVertices; i++)
{
    // Schritt 1: Index desjenigen Gelenks suchen und speichern, das den
    // kleinsten quadrat. Abstand vom betrachteten Vertex aufweist:

    // größtmöglicher quadrat. Abstand
    minDistanceSq1 = 100000000.0f;
    tempVertexID1 = -1;

    for(j = 0; j < numJoints; j++)
    {
        tempVektor3.x = pNonAnimatedJointModelSpacePosArray[j].x -
                        VertexArray[i].PosX;
        tempVektor3.y = pNonAnimatedJointModelSpacePosArray[j].y -
                        VertexArray[i].PosY;
        tempVektor3.z = pNonAnimatedJointModelSpacePosArray[j].z -
                        VertexArray[i].PosZ;

        distanceSq = D3DXVec3LengthSq(&tempVektor3);

        if(distanceSq < minDistanceSq1)
        {
            minDistanceSq1 = distanceSq;
            tempVertexID1  = j;
        }
    }

    // Schritt 2: Überprüfen, zu welchen Knochen der betrachtete Vertex
    // gehören
könnte. Quadrat. Abstände zwischen Vertex und Endgelenken
    // bestimmen und den Index desjenigen Endgelenks mit dem kleinsten
    // quadrat. Abstand speichern:

    // größtmöglicher quadrat. Abstand
    minDistanceSq2 = 100000000.0f;
    tempVertexID2 = -1;

    for(j = 0; j < numBones; j++)
    {
        if(tempVertexID1 == pBoneDescArray[j].NrJoint2)
        {
            temp1Vektor3.x = VertexArray[i].PosX -
            pNonAnimatedJointModelSpacePosArray[
            pBoneDescArray[j].NrJoint2].x;

            temp1Vektor3.y = VertexArray[i].PosY -
            pNonAnimatedJointModelSpacePosArray[
            pBoneDescArray[j].NrJoint2].y;

            temp1Vektor3.z = VertexArray[i].PosZ -
            pNonAnimatedJointModelSpacePosArray[
            pBoneDescArray[j].NrJoint2].z;

            temp2Vektor3 = pNonAnimatedJointModelSpacePosArray[
                           pBoneDescArray[j].NrJoint1] -
                           pNonAnimatedJointModelSpacePosArray[
                           pBoneDescArray[j].NrJoint2];

            if(D3DXVec3Dot(&temp1Vektor3, &temp2Vektor3) >= 0.0f)
            {
                distanceSq = D3DXVec3LengthSq(&temp1Vektor3);

                if(distanceSq <= minDistanceSq2)
                {
                    minDistanceSq2 = distanceSq;
                    tempVertexID2 = tempVertexID1;
                }
            }
        }
    }

    // Schritt 3: Falls die Zuordung im zweiten Schritt noch nicht
    // erfolgreich
war, alle Knochen durchgehen und überprüfen, ob
    // das im ersten Schritt
gefundene Gelenk ein Drehgelenk ist.
    // Sollte dies der Fall sein, den Index des Endgelenks des
    // betreffenden Knochens speichern:

    if(tempVertexID2 == -1)
    {
        for(j = 0; j < numBones; j++)
        {
            if(tempVertexID1 == pBoneDescArray[j].NrJoint1)
            {
                tempVertexID2 = pBoneDescArray[j].NrJoint2;
                break;
            }
        }
    }

    // Schritt 4: Sollte die Zuordnung immer noch nicht erfolgreich sein,
    // dann
einfach den Index des im ersten Schritt gefundenen Gelenks
    // verwenden:

    if(tempVertexID2 == -1)
        tempVertexID2 = tempVertexID1;

    // Vertexposition relativ zum zugehörigen Gelenk berechnen:
    VertexArray[i].PosX = VertexArray[i].PosX -
                          pNonAnimatedJointModelSpacePosArray[
                          tempVertexID2].x;

    VertexArray[i].PosY = VertexArray[i].PosY -
                          pNonAnimatedJointModelSpacePosArray[
                          tempVertexID2].y;

    VertexArray[i].PosZ = VertexArray[i].PosZ -
                          pNonAnimatedJointModelSpacePosArray[
                          tempVertexID2].z;

    // Indices der beim indizierten Vertex Blending zu verwendenden
    // Transformationsmatrizen festlegen:
    VertexArray[i].tw = (float)tempVertexID2;
}

// Nun müssen wir noch dafür Sorge tragen, dass im Zuge der Beleuchtung
// alle Normalen der zu einem Dreieck zugehörigen Vertices im
// Vertex Shader auf gleiche Weise transformiert werden
// (sieht besser aus)

long index1, index2, index3;

for(i = 0; i < MeshIndexData[0].NumIndices; i+=3)
{
    // alle dreiecke werden nacheinander durchgegangen:
    index1 = IndexArray[i];
    index2 = IndexArray[i+1];
    index3 = IndexArray[i+2];

    VertexArray[index1].ta = VertexArray[index1].tw;
    VertexArray[index2].ta = VertexArray[index1].tw;
    VertexArray[index3].ta = VertexArray[index1].tw;
}

glGenBuffers(1, &MeshVBId);
glBindBuffer(GL_ARRAY_BUFFER, MeshVBId);
glBufferData(GL_ARRAY_BUFFER,
             NumMeshVertices*sizeof(CTexturedAnimatedVertexWithNormal_NM),
             VertexArray, GL_STATIC_DRAW);

Hinweis zur Treffer- und Kollisionserkennung
Nun da sie wissen, wie man ein 3D-Modell an ein Animations-Skelett anbinden kann, stellt sich noch die Frage, wie man mit dem zum Modell zugehörigen Kollisionsmodell verfahren sollte. Die pragmatische Antwort lautet – vergessen sie das Kollisionsmodell!
Das Animations-Skelett lässt sich ebenso für die Durchführung von Kollisions- und Treffertests verwenden. Sie müssen lediglich für jedes Gelenk eine Bounding-Sphäre definieren. Unter Berücksichtigung der aktuellen Worldspace-Positionen der animierten Gelenke können sie dann zu jeder Zeit Bounding-Sphären-Treffer- und -Kollisionstests durchführen.