Animation von 3D-Modellen Teil 05: Animationssequenzen

Im heutigen Artikel werden wir uns mit der Beschreibung der Animationssequenzen befassen. Hierbei müssen wir zunächst klären, welche Daten es in diesem Zusammenhang zu speichern gilt. Schauen wir uns mal eine einfache Laufsequenz an:

NumAnimationPoses: 12

TimeBetweenTwoAnimationPoses[ms]: 90

BodyPosition: 0.0, 0.0, 0.0
MovementVelocityFactor: 2.4
HumanBody/WALK01.txt

BodyPosition: 0.0, -0.05, 0.0
MovementVelocityFactor: 0.8
HumanBody/WALK02.txt

. . .

BodyPosition: 0.0, -0.05, 0.0
MovementVelocityFactor: 0.8
HumanBody/WALK12.txt

Zunächst einmal gilt es, die Anzahl der Animationsposen sowie die Animationsgeschwindigkeit (TimeBetweenTwoAnimationPoses) festzulegen. Für jede Pose muss zudem die Position des animierten 3D-Modells in Modellkoordinaten angegeben werden, da sich im Verlauf der Animation natürlich auch die Lage des Schwerpunkts ändern kann – bestes Beispiel hierfür ist eine Sprung-Animation. Charakteristisch für eine Animation ist zudem die Geschwindigkeit (MovementVelocityFactor), mit der sich das 3D-Modell währenddessen fortbewegt. Für eine Sprint-Animation muss man hier deutlich größere Werte festlegen als für eine Lauf-Animation. Auf Basis dieser Faktoren kann dann die Bewegung des 3D-Modells simuliert werden:

WorldSpacePosition += WalkingSequence->InterpolatedBodyPosition +
                      WalkingSequence->InterpolatedMovementVelocityFactor*
                      WalkingDirection;

Je nach Art einer Animationssequenz wird diese entweder als Schleife (z. B. eine Lauf-Animation) oder nur einmalig (z. B. eine Sprung-Animation) abgespielt. Zudem ist es sinnvoll, jeder Animation diverse Geräusch-, Partikel- und Lichteffekte zuzuordnen. Es bietet sich daher an, sämtliche zu ladende Animationen samt Zusatzinformationen in einer übergeordneten Datei aufzulisten.

Die nachfolgende Update_SkeletalAnimationSequence()-Methode ermittelt nun die aktuelle Key-Frame-Pose und behandelt dabei jede Sequenz als Schleifen-Animation. Anhand der aktuellen Nummer der Animationspose lässt sich jedoch feststellen, ob bereits alle Key Frames durchlaufen sind (CurrentAnimationPoseNr == 0). Einmal-Animationen (z. B. eine Sprung-Animation) lassen sich auf diese Weise rechtzeitig stoppen.

void CSkeletalAnimationSequence::Update_SkeletalAnimationSequence(void)
{
    long ActualTime = GetTickCount();

    long TimePeroidSinceLastAnimationPoseChange = ActualTime -
         TimeOfLastAnimationPoseChange;

    // Falls es an der Zeit ist, dann eine neue Animationspose festlegen:
    if(TimePeroidSinceLastAnimationPoseChange >=
       TimeBetweenTwoAnimationPoses)
    {
        CurrentAnimationPoseNr++;
        NextAnimationPoseNr++;

        TimeOfLastAnimationPoseChange = ActualTime;
    }

    if(CurrentAnimationPoseNr == NumAnimationPoses)
        CurrentAnimationPoseNr = 0;

    if(NextAnimationPoseNr == NumAnimationPoses)
        NextAnimationPoseNr = 0;
}

Bereits im ersten Teil dieser Artikelserie haben wir darüber gesprochen, dass man lediglich die Key Frames der einzelnen Animationssequenzen speichert und die aktuelle Animationspose zur Laufzeit aus je zwei Key-Frame-Posen interpoliert. Hierbei werden in Abhängigkeit vom Zeitpunkt des letzten Wechsels der Animationspose zunächst zwei Gewichtungsfaktoren ermittelt und mit diesen dann die gewichteten Mittel der Gelenkwinkel, der Bewegungsgeschwindigkeiten sowie der Modelkoordinaten-Positionen berechnet:

void CSkeletalAnimationSequence::
     SkeletalAnimationSequence_Interpolation(void)
{
long
ActualTime = GetTickCount();

long TimePeroidSinceLastAnimationPoseChange = ActualTime -
                                              TimeOfLastAnimationPoseChange;

// Berechnung der Gewichtungsfaktoren für die Interpolation:
float PoseNr2Weight = (float)TimePeroidSinceLastAnimationPoseChange/
                      (float)TimeBetweenTwoAnimationPoses;

float PoseNr1Weight = 1.0f - PoseNr2Weight;

for(long i = 0; i < NumAnimatedJoints; i++)
{
       InterpolatedAnimationPose->JointRotationAngleX[i] = PoseNr1Weight*
        AnimationPose[CurrentAnimationPoseNr].JointRotationAngleX[i] +
        PoseNr2Weight*
        AnimationPose[NextAnimationPoseNr].JointRotationAngleX[i];

    InterpolatedAnimationPose->JointRotationAngleY[i] = PoseNr1Weight*
        AnimationPose[CurrentAnimationPoseNr].JointRotationAngleY[i] +
        PoseNr2Weight*
        AnimationPose[NextAnimationPoseNr].JointRotationAngleY[i];

    InterpolatedAnimationPose->JointRotationAngleZ[i] = PoseNr1Weight*
        AnimationPose[CurrentAnimationPoseNr].JointRotationAngleZ[i] +
        PoseNr2Weight*
        AnimationPose[NextAnimationPoseNr].JointRotationAngleZ[i];
}

InterpolatedMovementVelocityFactor = PoseNr1Weight*
            MovementVelocityFactor[CurrentAnimationPoseNr] +
            PoseNr2Weight*
            MovementVelocityFactor[NextAnimationPoseNr];

InterpolatedBodyPosition = PoseNr1Weight*
                           BodyPosition[CurrentAnimationPoseNr] +
                           PoseNr2Weight*BodyPosition[NextAnimationPoseNr];
}

Die SkeletalAnimationSequence_Interpolation()-Methode dient lediglich zur Interpolation der aktuellen Animationspose der laufenden Animationssequenz. Zusätzlich dazu benötigen wir noch eine zweite Interpolationsfunktion, mit deren Hilfe man bei einem Wechsel der Animationssequenzen so genannte Übergangsposen generieren kann:

void Combine_Two_SkeletalAnimationPoses(
     CSkeletalAnimationPose* pInterpolatedAnimationPose,
     D3DXVECTOR3* pInterpolatedBodyPosition,
     float* pInterpolatedMovementVelocityFactor,

     CSkeletalAnimationPose* pAnimationPose1, float PoseNr1Weight,
     float PoseNr1MovementVelocityFactor,D3DXVECTOR3* pPoseNr1BodyPosition,

     CSkeletalAnimationPose* pAnimationPose2, float PoseNr2Weight,
     float PoseNr2MovementVelocityFactor,D3DXVECTOR3* pPoseNr2BodyPosition)
{
    for(long i = 0; i < pAnimationPose1->NumJoints; i++)
    {
        pInterpolatedAnimationPose->JointRotationAngleX[i] =
        PoseNr1Weight*pAnimationPose1->JointRotationAngleX[i] +
        PoseNr2Weight*pAnimationPose2->JointRotationAngleX[i];

        pInterpolatedAnimationPose->JointRotationAngleY[i] =
        PoseNr1Weight*pAnimationPose1->JointRotationAngleY[i] +
        PoseNr2Weight*pAnimationPose2->JointRotationAngleY[i];

        pInterpolatedAnimationPose->JointRotationAngleZ[i] =
        PoseNr1Weight*pAnimationPose1->JointRotationAngleZ[i] +
        PoseNr2Weight*pAnimationPose2->JointRotationAngleZ[i];
    }

    *pInterpolatedMovementVelocityFactor =
      PoseNr1Weight*PoseNr1MovementVelocityFactor +
      PoseNr2Weight*PoseNr2MovementVelocityFactor;

    *pInterpolatedBodyPosition = PoseNr1Weight*(*pPoseNr1BodyPosition) +
                                 PoseNr2Weight*(*pPoseNr2BodyPosition);
}